Workflows
A workflow is a versioned DAG definition. Once published, it's immutable in the sense that changing it bumps the version — running executions continue with the version they started under.
Workflow object
{
"id": "wkf_01HXY...",
"slug": "indonesian-kyc-v1",
"name": "Indonesian KYC onboarding",
"description": "...",
"spec": { /* DAG, see below */ },
"triggerType": "api | webhook | schedule",
"triggerSpec": null,
"status": "draft | active | retired",
"version": 3,
"createdAt": "...",
"updatedAt": "..."
}Status lifecycle
draft → (publish) → active → (retire) → retired
↑
(PATCH the spec)
│
└→ draft (auto-rolls back to draft on spec change)A workflow can be referenced by its id or its slug (/api/workflows/indonesian-kyc-v1). Slugs are unique per org.
Spec shape
{
"startNodeId": "step_a",
"nodes": [
{ "id": "step_a", "type": "service_call", ... },
{ "id": "step_b", "type": "decision", ... },
{ "id": "step_c", "type": "transform", ... },
{ "id": "step_d", "type": "human_approval", ... }
],
"edges": [
{ "from": "step_a", "to": "step_b" },
{ "from": "step_b", "to": "step_c", "case": "default" },
{ "from": "step_b", "to": "step_d", "case": "needs_review" },
{ "from": "step_c", "to": "step_d" }
]
}Cycles are rejected — workflows are strictly DAGs.
Node types
service_call
{
"id": "kyc",
"type": "service_call",
"service": "identity",
"method": "POST",
"path": "/api/identity/document/ktp/capture",
"headers": { "X-Trace-Id": "${trigger.traceId}" },
"body": {
"customerId": "${trigger.customerId}",
"bundle": "${trigger.bundle}"
},
"retry": {
"maxAttempts": 3,
"backoff": "exponential",
"retryOn": ["5xx", "timeout"]
},
"timeoutMs": 30000
}| Field | Notes |
|---|---|
service | A registered service connection slug (identity, aml, or a custom one). |
method | HTTP verb. |
path | Path on the target. ${...} templating allowed. |
headers | Additional headers. Auth (Bearer key) is auto-injected from the service connection. |
body | Request body. Full templating; objects/arrays preserved structurally. |
retry | Override per-node retry policy. Defaults: 3 attempts, exponential, 5xx + timeout. |
timeoutMs | Per-attempt timeout. Default 30 seconds. |
The step output is the parsed response body of the call. Use ${step.kyc.output.fieldName} downstream.
decision
{
"id": "route",
"type": "decision",
"expression": "step.kyc.output.ktpBundle.verdict",
"cases": {
"passed": { "next": "screening" },
"requires_review": { "next": "wait_for_analyst" },
"failed": { "next": "notify_failure" }
},
"defaultCase": "notify_failure"
}expression is a dot-path expression over the prior-step output. cases is a string→target map. If no case matches, defaultCase fires (required — workflows reject ambiguity).
transform
{
"id": "shape_payload",
"type": "transform",
"expression": {
"applicantName": "${step.kyc.output.ktpBundle.ocr.fullName}",
"applicantNik": "${step.kyc.output.ktpBundle.ocr.nik}",
"applicantDob": "${step.kyc.output.ktpBundle.ocr.dateOfBirth}"
}
}Pure shaping. Output is the evaluated expression. Useful for adapting one sibling product's output shape to another's input shape.
wait
{
"id": "delay",
"type": "wait",
"duration": "PT5M"
}ISO-8601 duration. Or:
{ "id": "until_morning", "type": "wait", "until": "${trigger.scheduledAt}" }For long waits (>1 hour), the execution is hibernated — no compute cost while waiting.
webhook_emit
{
"id": "notify",
"type": "webhook_emit",
"event": "kyc.completed",
"payload": {
"customerId": "${trigger.customerId}",
"kycLevel": "${step.kyc.output.customer.kycLevel}"
}
}Fires an outbound webhook to whoever subscribed to this event on the workflow's outbound webhook list. Distinct from registering a per-execution callback.
human_approval
{
"id": "wait_for_analyst",
"type": "human_approval",
"actionLabel": "KTP requires manual review",
"assignTo": "role:kyc_analyst | user:usr_01HXY... | team:tm_01HXY...",
"timeoutHours": 24
}Run pauses. See Approvals →.
CRUD endpoints
/api/workflows/api/workflows/api/workflows/{id-or-slug}/api/workflows/{id-or-slug}/api/workflows/{id-or-slug}/publishPATCH resets the workflow to draft and bumps version. Active executions on the prior version keep running; new triggers go to the new draft (unsigned, blocked) until republished.
Listing
GET /api/workflows?status=active&limit=50{
"data": {
"workflows": [
{
"id": "wkf_01HXY...",
"slug": "indonesian-kyc-v1",
"name": "Indonesian KYC onboarding",
"status": "active",
"triggerType": "api",
"version": 3,
"_count": { "executions": 4827 }
}
]
}
}Workflows are immutable in practice
When you PATCH the spec, in-flight executions don't migrate to the new version. They complete under the old version. Plan for this: name versions explicitly (-v1, -v2) and migrate callers, rather than mutating in place.