feat(workflows): M5 — SDK workflow::start + admin run API + pic CLI
The user-facing surface for the durable DAG engine. A workflow run can now be
started three ways, all resolving the workflow by name in the caller's app
(`cx.app_id` / the resolved app — never a passed-in arg, the isolation
boundary), seeding a run the orchestrator advances.
SDK seam (shared):
- `WorkflowService` trait + `NoopWorkflowService` + `WorkflowError` in
`shared::workflow` (mirrors `InvokeService`); added to `Services` as a
noop-defaulted `with_workflow` builder (all `Services::new` call sites
unchanged).
- `WorkflowServiceImpl` (manager-core): resolves + seeds a run; authenticated
callers gated on `AppInvoke` (anonymous skips, script-as-gate), the same gate
`invoke()` uses.
- `executor-core::sdk::workflow` — the Rhai bridge `workflow::start(name, input)`
/ `workflow::start(name)`, registered in `register_all`. Returns the run id.
Admin API (manager-core `workflows_api`, mounted in the binary):
- `GET /apps/{app}/workflows` — definitions (AppRead)
- `POST /apps/{app}/workflows/{name}/runs` — start a run (AppInvoke)
- `GET /apps/{app}/workflows/{name}/runs` — run history (AppRead)
- `GET /apps/{app}/workflow-runs/{run_id}` — one run + its steps (AppRead)
`app_id` scopes every query; a foreign run 404s.
CLI (`pic workflows`): `ls` · `run <name> [--input JSON]` · `runs <name>` ·
`run-status <run_id>` — all `--app`-scoped; run definitions stay declarative
(`[[workflows]]` → `pic apply`).
Also: `list_runs_for_workflow` repo reader.
Tests: `workflow_service_start_seeds_a_run` (DB-gated — the SDK/API-shared
entry) + `workflow_run_executes_end_to_end` CLI journey (apply → `pic workflows
run` → poll `run-status` → succeeded, through the real binary + orchestrator).
fmt + clippy -D warnings clean, 449 manager-core lib tests, 14 orchestrator DB
tests, 18 executor-core lib tests, 3 workflow CLI journeys green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -624,6 +624,65 @@ impl Client {
|
||||
decode(resp).await
|
||||
}
|
||||
|
||||
/// `GET /api/v1/admin/apps/{id}/workflows`
|
||||
pub async fn workflows_list(&self, app: &str) -> Result<Vec<WorkflowDto>> {
|
||||
let app = seg(app);
|
||||
let resp = self
|
||||
.request(Method::GET, &format!("/api/v1/admin/apps/{app}/workflows"))
|
||||
.send()
|
||||
.await?;
|
||||
decode(resp).await
|
||||
}
|
||||
|
||||
/// `POST /api/v1/admin/apps/{id}/workflows/{name}/runs` — start a run.
|
||||
pub async fn workflow_run_start(
|
||||
&self,
|
||||
app: &str,
|
||||
name: &str,
|
||||
input: serde_json::Value,
|
||||
) -> Result<WorkflowRunDto> {
|
||||
let (app, name) = (seg(app), seg(name));
|
||||
let resp = self
|
||||
.request(
|
||||
Method::POST,
|
||||
&format!("/api/v1/admin/apps/{app}/workflows/{name}/runs"),
|
||||
)
|
||||
.json(&serde_json::json!({ "input": input }))
|
||||
.send()
|
||||
.await?;
|
||||
decode(resp).await
|
||||
}
|
||||
|
||||
/// `GET /api/v1/admin/apps/{id}/workflows/{name}/runs` — recent runs.
|
||||
pub async fn workflow_runs_list(&self, app: &str, name: &str) -> Result<Vec<WorkflowRunDto>> {
|
||||
let (app, name) = (seg(app), seg(name));
|
||||
let resp = self
|
||||
.request(
|
||||
Method::GET,
|
||||
&format!("/api/v1/admin/apps/{app}/workflows/{name}/runs"),
|
||||
)
|
||||
.send()
|
||||
.await?;
|
||||
decode(resp).await
|
||||
}
|
||||
|
||||
/// `GET /api/v1/admin/apps/{id}/workflow-runs/{run_id}` — one run + steps.
|
||||
pub async fn workflow_run_status(
|
||||
&self,
|
||||
app: &str,
|
||||
run_id: &str,
|
||||
) -> Result<WorkflowRunDetailDto> {
|
||||
let (app, run_id) = (seg(app), seg(run_id));
|
||||
let resp = self
|
||||
.request(
|
||||
Method::GET,
|
||||
&format!("/api/v1/admin/apps/{app}/workflow-runs/{run_id}"),
|
||||
)
|
||||
.send()
|
||||
.await?;
|
||||
decode(resp).await
|
||||
}
|
||||
|
||||
/// `DELETE /api/v1/admin/apps/{id}/triggers/{trigger_id}`
|
||||
pub async fn triggers_delete(&self, app: &str, trigger_id: &str) -> Result<()> {
|
||||
let (app, trigger_id) = (seg(app), seg(trigger_id));
|
||||
@@ -2202,6 +2261,41 @@ struct UpdateScriptBody<'a> {
|
||||
// columns are a one-line add), but the TSV/KvBlock renderers don't print
|
||||
// every field today.
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct WorkflowDto {
|
||||
pub name: String,
|
||||
pub enabled: bool,
|
||||
pub steps: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct WorkflowRunDto {
|
||||
pub id: String,
|
||||
pub status: String,
|
||||
#[serde(default)]
|
||||
pub error: Option<String>,
|
||||
pub workflow_depth: i32,
|
||||
pub created_at: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct WorkflowRunStepDto {
|
||||
pub name: String,
|
||||
pub status: String,
|
||||
pub attempt: i32,
|
||||
pub max_attempts: i32,
|
||||
#[serde(default)]
|
||||
pub error: Option<String>,
|
||||
#[serde(default)]
|
||||
pub child_run_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct WorkflowRunDetailDto {
|
||||
pub run: WorkflowRunDto,
|
||||
pub steps: Vec<WorkflowRunStepDto>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct AppMemberDto {
|
||||
pub user_id: AdminUserId,
|
||||
|
||||
Reference in New Issue
Block a user