📘 Public beta · Endpoints are stable; OpenAPI specs and SDKs ship monthly. See changelog →
Products
Orchestration
Workflows

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
}
FieldNotes
serviceA registered service connection slug (identity, aml, or a custom one).
methodHTTP verb.
pathPath on the target. ${...} templating allowed.
headersAdditional headers. Auth (Bearer key) is auto-injected from the service connection.
bodyRequest body. Full templating; objects/arrays preserved structurally.
retryOverride per-node retry policy. Defaults: 3 attempts, exponential, 5xx + timeout.
timeoutMsPer-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

POST/api/workflows
Auth · API keyScope · workflows:write
GET/api/workflows
Auth · API keyScope · workflows:read
GET/api/workflows/{id-or-slug}
Auth · API keyScope · workflows:read
PATCH/api/workflows/{id-or-slug}
Auth · API keyScope · workflows:write
POST/api/workflows/{id-or-slug}/publish
Auth · API keyScope · workflows:write

PATCH 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.