Skip to content

API Reference#

Complete REST API reference for the Orch8 Engine. All endpoints accept and return JSON. Dates use ISO 8601 / RFC 3339 format.

Base URLhttp://localhost:8080

Configurable via ORCH8_HTTP_ADDR environment variable.

Health#

Liveness Probe#

GET/health/live

Always returns 200 if the process is running.

Response: 200 OK

Readiness Probe#

GET/health/ready

Returns 200 if the database is reachable.

Response: 200 OK

or 503 Service Unavailable

Sequences#

Create Sequence#

POST/sequences

Request Body

JSON
{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "tenant_id": "acme",
  "namespace": "default",
  "name": "welcome-campaign",
  "version": 1,
  "blocks": [
    {
      "type": "step",
      "id": "send_welcome",
      "handler": "http_request",
      "params": { "url": "https://api.example.com/send", "method": "POST", "body": { "template": "welcome" } },
      "retry": {
        "max_attempts": 3,
        "initial_backoff": "1s",
        "max_backoff": "60s",
        "backoff_multiplier": 2.0
      },
      "timeout": "30s"
    },
    {
      "type": "step",
      "id": "wait_3_days",
      "handler": "sleep",
      "params": { "duration_ms": 259200000 }
    },
    {
      "type": "router",
      "id": "check_engagement",
      "routes": [
        {
          "condition": "context.data.opened == true",
          "blocks": [
            {
              "type": "step",
              "id": "send_followup",
              "handler": "http_request",
              "params": { "url": "https://api.example.com/send", "method": "POST", "body": { "template": "followup" } }
            }
          ]
        }
      ],
      "default": [
        {
          "type": "step",
          "id": "send_reminder",
          "handler": "http_request",
          "params": { "url": "https://api.example.com/send", "method": "POST", "body": { "template": "reminder" } }
        }
      ]
    }
  ],
  "created_at": "2024-01-15T10:00:00Z"
}

Response: 201 Created

JSON
{
  "id": "550e8400-e29b-41d4-a716-446655440000"
}

Get Sequence#

GET/sequences/{id}

Response: 200 OK

Returns the full SequenceDefinition object.

Get Sequence by Name#

GET/sequences/by-name?tenant_id=acme&namespace=default&name=welcome-campaign&version=1
ParameterTypeRequiredDescription
tenant_idstringYesTenant identifier
namespacestringYesNamespace
namestringYesSequence name
versionintegerNoSpecific version (latest if omitted)

Response: 200 OK

Returns the full SequenceDefinition object.

Instances#

Create Instance#

POST/instances

Request Body

JSON
{
  "sequence_id": "550e8400-e29b-41d4-a716-446655440000",
  "tenant_id": "acme",
  "namespace": "default",
  "priority": "normal",
  "timezone": "America/New_York",
  "metadata": { "campaign": "spring-2024", "segment": "enterprise" },
  "context": {
    "data": { "email": "john@acme.com", "name": "John" },
    "config": { "sender": "noreply@orch8.io" }
  },
  "next_fire_at": "2024-01-15T14:00:00Z",
  "concurrency_key": "contact:john@acme.com",
  "max_concurrency": 1,
  "idempotency_key": "welcome:john@acme.com:2024-01-15"
}
ParameterTypeRequiredDescription
sequence_idUUIDYesSequence to execute
tenant_idstringYesTenant identifier
namespacestringYesNamespace
prioritystringNolow, normal, high, critical. Default: "normal"
timezonestringNoIANA timezone. Default: "UTC"
metadataobjectNoArbitrary JSON metadata
contextobjectNoExecution context (data, config sections)
next_fire_atdatetimeNoWhen to start execution. Default: now
concurrency_keystringNoKey for concurrency limiting
max_concurrencyintegerNoMax parallel instances with same key
idempotency_keystringNoDeduplication key

Response: 201 Created

JSON
{
  "id": "a1b2c3d4-...",
  "deduplicated": false
}

If idempotency_key matches an existing instance:

JSON
{
  "id": "existing-instance-id",
  "deduplicated": true
}

Create Instances (Batch)#

POST/instances/batch

Request Body

JSON
{
  "instances": [
    { "sequence_id": "...", "tenant_id": "acme", "namespace": "default" },
    { "sequence_id": "...", "tenant_id": "acme", "namespace": "default" }
  ]
}

Response: 201 Created

JSON
{
  "count": 2
}

Get Instance#

GET/instances/{id}

Response: 200 OK

JSON
{
  "id": "a1b2c3d4-...",
  "sequence_id": "550e8400-...",
  "tenant_id": "acme",
  "namespace": "default",
  "state": "running",
  "next_fire_at": "2024-01-15T14:00:00Z",
  "priority": "normal",
  "timezone": "America/New_York",
  "metadata": { "campaign": "spring-2024" },
  "context": {
    "data": { "email": "john@acme.com" },
    "config": {},
    "audit": [],
    "runtime": { "current_step": "send_welcome", "attempt": 0 }
  },
  "concurrency_key": "contact:john@acme.com",
  "max_concurrency": 1,
  "idempotency_key": "welcome:john@acme.com:2024-01-15",
  "created_at": "2024-01-15T10:00:00Z",
  "updated_at": "2024-01-15T14:00:05Z"
}

List Instances#

GET/instances?tenant_id=acme&namespace=default&state=running,scheduled&limit=50&offset=0
ParameterTypeRequiredDescription
tenant_idstringNoFilter by tenant
namespacestringNoFilter by namespace
sequence_idUUIDNoFilter by sequence
statestringNoComma-separated states to include
offsetintegerNoPagination offset. Default: 0
limitintegerNoPage size (max 1000). Default: 100

Response: 200 OK

Returns an array of TaskInstance objects.

Update Instance State#

PATCH/instances/{id}/state

Request Body

JSON
{
  "state": "paused",
  "next_fire_at": "2024-01-16T09:00:00Z"
}

Valid state transitions

FromTo
ScheduledRunning, Paused, Cancelled
RunningScheduled, Waiting, Completed, Failed, Paused, Cancelled
WaitingRunning, Scheduled, Cancelled, Failed
PausedScheduled, Cancelled
FailedScheduled (retry)
Completed(terminal)
Cancelled(terminal)

Response: 200 OK

Returns 400 Bad Request if the transition is invalid.

Update Instance Context#

PATCH/instances/{id}/context

Request Body

JSON
{
  "context": {
    "data": { "opened": true, "clicked_link": "pricing" }
  }
}

Response: 200 OK

Send Signal#

POST/instances/{id}/signals

Request Body

JSON
{
  "signal_type": "pause",
  "payload": {}
}
Signal TypeEffect
pausePause execution
resumeResume from Paused
cancelCancel instance
update_contextMerge payload into context.data
custom:*Application-defined signal

Response: 201 Created

JSON
{
  "signal_id": "b5c6d7e8-..."
}

Get Outputs#

GET/instances/{id}/outputs

Response: 200 OK

JSON
[
  {
    "id": "f1e2d3c4-...",
    "instance_id": "a1b2c3d4-...",
    "block_id": "send_welcome",
    "output": { "email_id": "msg-abc123", "status": "sent" },
    "output_ref": null,
    "output_size": 52,
    "attempt": 0,
    "created_at": "2024-01-15T14:00:03Z"
  }
]

Retry Failed Instance#

POST/instances/{id}/retry

Instance must be in failed state. Resets to scheduled with next_fire_at = now.

Response: 200 OK

JSON
{
  "id": "a1b2c3d4-...",
  "state": "scheduled"
}

Bulk Update State#

PATCH/instances/bulk/state

Request Body

JSON
{
  "filter": {
    "tenant_id": "acme",
    "namespace": "default",
    "sequence_id": "550e8400-...",
    "states": ["scheduled", "running"]
  },
  "state": "cancelled"
}

Response: 200 OK

JSON
{
  "count": 47
}

List Dead Letter Queue#

GET/instances/dlq?tenant_id=acme&namespace=default&limit=50

Same parameters as List Instances. Returns only failed instances.

Response: 200 OK

Returns an array of TaskInstance objects.

Cron Schedules#

Create Cron Schedule#

POST/cron

Request Body

JSON
{
  "tenant_id": "acme",
  "namespace": "default",
  "sequence_id": "550e8400-...",
  "cron_expr": "0 9 * * MON-FRI *",
  "timezone": "America/New_York",
  "metadata": { "type": "daily-report" },
  "enabled": true
}
ParameterTypeRequiredDescription
tenant_idstringYesTenant identifier
namespacestringYesNamespace
sequence_idUUIDYesSequence to instantiate
cron_exprstringYes7-field cron expression
timezonestringNoIANA timezone for schedule. Default: "UTC"
metadataobjectNoPassed to created instances
enabledbooleanNoWhether schedule is active. Default: true

Cron expression format (7 fields)

JSON
second  minute  hour  day  month  day_of_week  year
  0       9      *     *     *      MON-FRI      *

Response: 201 Created

JSON
{
  "id": "c1d2e3f4-...",
  "next_fire_at": "2024-01-16T14:00:00Z"
}

Get Cron Schedule#

GET/cron/{id}

Response: 200 OK

Returns the full CronSchedule object.

List Cron Schedules#

GET/cron?tenant_id=acme

Response: 200 OK

Returns an array of CronSchedule objects.

Update Cron Schedule#

PUT/cron/{id}

Request Body (all fields optional)

JSON
{
  "cron_expr": "0 10 * * * *",
  "timezone": "Europe/London",
  "enabled": false,
  "metadata": { "type": "weekly-digest" }
}

Response: 200 OK

Returns the updated CronSchedule.

Delete Cron Schedule#

DELETE/cron/{id}

Response: 204 No Content

External Workers#

Poll for Tasks#

POST/workers/tasks/poll

Request Body

JSON
{
  "handler_name": "process_image",
  "worker_id": "node-worker-42",
  "limit": 10
}
ParameterTypeRequiredDescription
handler_namestringYesHandler to claim tasks for
worker_idstringYesUnique worker identifier
limitintegerNoMax tasks to claim. Default: 1

Response: 200 OK

JSON
[
  {
    "id": "d4e5f6a7-...",
    "instance_id": "a1b2c3d4-...",
    "block_id": "send_welcome",
    "handler_name": "process_image",
    "params": { "template": "welcome", "to": "john@acme.com" },
    "context": { "data": { "name": "John" }, "config": {} },
    "attempt": 0,
    "timeout_ms": 30000,
    "state": "claimed",
    "worker_id": "node-worker-42",
    "claimed_at": "2024-01-15T14:00:00Z",
    "heartbeat_at": "2024-01-15T14:00:00Z",
    "completed_at": null,
    "output": null,
    "error_message": null,
    "error_retryable": null,
    "created_at": "2024-01-15T13:59:58Z"
  }
]

Returns empty array [] if no tasks available.

Mechanics: Uses FOR UPDATE SKIP LOCKED — concurrent workers never get the same task. Sets state = claimed, worker_id, claimed_at, and heartbeat_at.

Complete Task#

POST/workers/tasks/{task_id}/complete

Request Body

JSON
{
  "worker_id": "node-worker-42",
  "output": {
    "email_id": "msg-abc123",
    "delivered": true
  }
}
ParameterTypeRequiredDescription
worker_idstringYesMust match the worker that claimed the task
outputobjectYesResult JSON (saved as BlockOutput)

Response: 200 OK

Side effects

  • 1. Worker task marked completed
  • 2. BlockOutput created with the output JSON
  • 3. Instance transitions Waiting -> Scheduled (immediate re-processing)
  • 4. If instance uses execution tree, corresponding node marked Completed

Fail Task#

POST/workers/tasks/{task_id}/fail

Request Body

JSON
{
  "worker_id": "node-worker-42",
  "message": "SMTP connection refused",
  "retryable": true
}
ParameterTypeRequiredDescription
worker_idstringYesMust match claimer
messagestringYesError description
retryablebooleanNoWhether the error is transient. Default: false

Response: 200 OK

Retryable failure

  • Worker task deleted (allows re-dispatch on next tick)
  • Instance reset to Scheduled

Permanent failure

  • If instance has composite blocks: execution node marked Failed, instance re-scheduled for evaluator (try-catch can recover)
  • If instance is step-only: instance marked Failed (enters DLQ)

Heartbeat Task#

POST/workers/tasks/{task_id}/heartbeat

Request Body

JSON
{
  "worker_id": "node-worker-42"
}

Response: 200 OK

Send heartbeats every 15-30 seconds for long-running tasks. Tasks without a heartbeat for 60 seconds are reclaimed by the reaper and returned to the queue.

Observability#

Prometheus Metrics#

GET/metrics

Returns Prometheus text format (v0.0.4).

Counters

MetricDescription
orch8_instances_claimed_totalInstances claimed by scheduler
orch8_instances_completed_totalInstances completed successfully
orch8_instances_failed_totalInstances failed (entered DLQ)
orch8_steps_executed_totalSteps executed
orch8_steps_failed_totalSteps that failed
orch8_steps_retried_totalRetry attempts
orch8_signals_delivered_totalSignals processed
orch8_rate_limits_exceeded_totalRate limit deferrals
orch8_recovery_stale_instances_totalStale instances recovered at startup
orch8_webhooks_sent_totalWebhooks delivered
orch8_webhooks_failed_totalWebhook delivery failures
orch8_cron_triggered_totalCron instances created

Histograms

MetricDescription
orch8_tick_duration_secondsScheduler tick latency
orch8_step_duration_secondsIndividual step execution time
orch8_instance_processing_secondsTotal instance processing time

Gauges

MetricDescription
orch8_queue_depthInstances claimed in current tick
orch8_active_tasksCurrently in-flight step executions

Block Definitions#

All blocks are defined in the blocks array of a sequence. Blocks can nest arbitrarily.

step

JSON
{
  "type": "step",
  "id": "unique_block_id",
  "handler": "handler_name",
  "params": {},
  "delay": {
    "duration": "3600s",
    "business_days_only": false,
    "jitter": "300s"
  },
  "retry": {
    "max_attempts": 3,
    "initial_backoff": "1s",
    "max_backoff": "60s",
    "backoff_multiplier": 2.0
  },
  "timeout": "30s",
  "rate_limit_key": "resource:identifier"
}

Built-in handlers

HandlerParamsOutput
noop(none){}
logmessage (string), level ("debug"/"info"/"warn"){ "message": "..." }
sleepduration_ms (integer, default 100){ "slept_ms": N }
http_requesturl, method ("GET"/"POST"/"PUT"/"DELETE"), body, timeout_ms (default 10000){ "status": 200, "body": "..." }

Any handler name not registered as built-in is automatically dispatched to the external worker queue.

parallel

JSON
{
  "type": "parallel",
  "id": "notify_all",
  "branches": [
    [{ "type": "step", "id": "email", "handler": "http_request", "params": { "url": "https://api.example.com/email", "method": "POST" } }],
    [{ "type": "step", "id": "sms", "handler": "http_request", "params": { "url": "https://api.example.com/sms", "method": "POST" } }],
    [{ "type": "step", "id": "push", "handler": "http_request", "params": { "url": "https://api.example.com/push", "method": "POST" } }]
  ]
}

All branches run concurrently. Completes when all finish. Fails if any branch fails.

race

JSON
{
  "type": "race",
  "id": "fastest_provider",
  "branches": [
    [{ "type": "step", "id": "provider_a", "handler": "send_via_a", "params": {} }],
    [{ "type": "step", "id": "provider_b", "handler": "send_via_b", "params": {} }]
  ],
  "semantics": "first_to_succeed"
}
SemanticsBehavior
first_to_resolveFirst branch to complete (success or failure) wins
first_to_succeedFirst successful branch wins; failures ignored until all fail

first_to_resolve is the default. Losing branches are cancelled.

router

JSON
{
  "type": "router",
  "id": "segment_users",
  "routes": [
    {
      "condition": "context.data.plan == 'enterprise'",
      "blocks": [{ "type": "step", "id": "vip_flow", "handler": "vip_onboard", "params": {} }]
    },
    {
      "condition": "context.data.plan == 'pro'",
      "blocks": [{ "type": "step", "id": "pro_flow", "handler": "pro_onboard", "params": {} }]
    }
  ],
  "default": [
    { "type": "step", "id": "free_flow", "handler": "free_onboard", "params": {} }
  ]
}

Evaluates conditions in order against context.data. First match wins. Falls through to default if none match.

Condition syntax: path == value (equality) or path (truthy check). Dot notation for nested paths (e.g., context.data.user.plan == 'enterprise').

try_catch

JSON
{
  "type": "try_catch",
  "id": "safe_send",
  "try_block": [
    { "type": "step", "id": "primary", "handler": "smtp_send", "params": {} }
  ],
  "catch_block": [
    { "type": "step", "id": "fallback", "handler": "ses_send", "params": {} }
  ],
  "finally_block": [
    { "type": "step", "id": "log_result", "handler": "log", "params": { "message": "send attempted" } }
  ]
}
  • try_block — Executes first. If all steps succeed, catch is skipped.
  • catch_block — Executes only if try failed. If catch succeeds, the overall block succeeds.
  • finally_block — Always executes, regardless of try/catch outcome. Optional.

loop

JSON
{
  "type": "loop",
  "id": "poll_status",
  "condition": "status.pending",
  "body": [
    { "type": "step", "id": "check", "handler": "http_request", "params": { "url": "https://api.example.com/status" } }
  ],
  "max_iterations": 50
}

Repeats body while condition evaluates to truthy in context.data. Safety cap via max_iterations (default 1000).

for_each

JSON
{
  "type": "for_each",
  "id": "process_batch",
  "collection": "items",
  "item_var": "item",
  "body": [
    { "type": "step", "id": "process", "handler": "process_item", "params": {} }
  ],
  "max_iterations": 500
}

Iterates over context.data[collection] (must be an array). Each iteration has item_var available in context. Empty or missing collection completes immediately.

Error Responses#

All error responses follow this format:

JSON
{
  "error": "Human-readable error message"
}
Status CodeMeaning
400Bad request (invalid JSON, missing required field, invalid state transition)
404Resource not found (instance, sequence, cron schedule, worker task)
409Conflict (state transition not allowed from current state)
500Internal server error

Ready to try Orch8?

One command to install. Two minutes to your first workflow.

Bash
curl -fsSL https://orch8.io/start.sh | sh