API Reference
Complete REST API reference for the Orch8 Engine. All endpoints accept and return JSON. Dates use ISO 8601 / RFC 3339 format.
Configurable via ORCH8_HTTP_ADDR environment variable.
Health
Liveness Probe
Always returns 200 if the process is running.
Response: 200 OK
Readiness Probe
Returns 200 if the database is reachable.
Response: 200 OK
or 503 Service Unavailable
Sequences
Create Sequence
Request Body
{
"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
{
"id": "550e8400-e29b-41d4-a716-446655440000"
}Get Sequence
Response: 200 OK
Returns the full SequenceDefinition object.
Get Sequence by Name
| Parameter | Type | Required | Description |
|---|---|---|---|
| tenant_id | string | Yes | Tenant identifier |
| namespace | string | Yes | Namespace |
| name | string | Yes | Sequence name |
| version | integer | No | Specific version (latest if omitted) |
Response: 200 OK
Returns the full SequenceDefinition object.
Instances
Create Instance
Request Body
{
"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"
}| Parameter | Type | Required | Description |
|---|---|---|---|
| sequence_id | UUID | Yes | Sequence to execute |
| tenant_id | string | Yes | Tenant identifier |
| namespace | string | Yes | Namespace |
| priority | string | No | low, normal, high, critical. Default: "normal" |
| timezone | string | No | IANA timezone. Default: "UTC" |
| metadata | object | No | Arbitrary JSON metadata |
| context | object | No | Execution context (data, config sections) |
| next_fire_at | datetime | No | When to start execution. Default: now |
| concurrency_key | string | No | Key for concurrency limiting |
| max_concurrency | integer | No | Max parallel instances with same key |
| idempotency_key | string | No | Deduplication key |
Response: 201 Created
{
"id": "a1b2c3d4-...",
"deduplicated": false
}If idempotency_key matches an existing instance:
{
"id": "existing-instance-id",
"deduplicated": true
}Create Instances (Batch)
Request Body
{
"instances": [
{ "sequence_id": "...", "tenant_id": "acme", "namespace": "default" },
{ "sequence_id": "...", "tenant_id": "acme", "namespace": "default" }
]
}Response: 201 Created
{
"count": 2
}Get Instance
Response: 200 OK
{
"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
| Parameter | Type | Required | Description |
|---|---|---|---|
| tenant_id | string | No | Filter by tenant |
| namespace | string | No | Filter by namespace |
| sequence_id | UUID | No | Filter by sequence |
| state | string | No | Comma-separated states to include |
| offset | integer | No | Pagination offset. Default: 0 |
| limit | integer | No | Page size (max 1000). Default: 100 |
Response: 200 OK
Returns an array of TaskInstance objects.
Update Instance State
Request Body
{
"state": "paused",
"next_fire_at": "2024-01-16T09:00:00Z"
}Valid state transitions
| From | To |
|---|---|
| Scheduled | Running, Paused, Cancelled |
| Running | Scheduled, Waiting, Completed, Failed, Paused, Cancelled |
| Waiting | Running, Scheduled, Cancelled, Failed |
| Paused | Scheduled, Cancelled |
| Failed | Scheduled (retry) |
| Completed | (terminal) |
| Cancelled | (terminal) |
Response: 200 OK
Returns 400 Bad Request if the transition is invalid.
Update Instance Context
Request Body
{
"context": {
"data": { "opened": true, "clicked_link": "pricing" }
}
}Response: 200 OK
Send Signal
Request Body
{
"signal_type": "pause",
"payload": {}
}| Signal Type | Effect |
|---|---|
| pause | Pause execution |
| resume | Resume from Paused |
| cancel | Cancel instance |
| update_context | Merge payload into context.data |
| custom:* | Application-defined signal |
Response: 201 Created
{
"signal_id": "b5c6d7e8-..."
}Get Outputs
Response: 200 OK
[
{
"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
Instance must be in failed state. Resets to scheduled with next_fire_at = now.
Response: 200 OK
{
"id": "a1b2c3d4-...",
"state": "scheduled"
}Bulk Update State
Request Body
{
"filter": {
"tenant_id": "acme",
"namespace": "default",
"sequence_id": "550e8400-...",
"states": ["scheduled", "running"]
},
"state": "cancelled"
}Response: 200 OK
{
"count": 47
}List Dead Letter Queue
Same parameters as List Instances. Returns only failed instances.
Response: 200 OK
Returns an array of TaskInstance objects.
Cron Schedules
Create Cron Schedule
Request Body
{
"tenant_id": "acme",
"namespace": "default",
"sequence_id": "550e8400-...",
"cron_expr": "0 9 * * MON-FRI *",
"timezone": "America/New_York",
"metadata": { "type": "daily-report" },
"enabled": true
}| Parameter | Type | Required | Description |
|---|---|---|---|
| tenant_id | string | Yes | Tenant identifier |
| namespace | string | Yes | Namespace |
| sequence_id | UUID | Yes | Sequence to instantiate |
| cron_expr | string | Yes | 7-field cron expression |
| timezone | string | No | IANA timezone for schedule. Default: "UTC" |
| metadata | object | No | Passed to created instances |
| enabled | boolean | No | Whether schedule is active. Default: true |
Cron expression format (7 fields)
second minute hour day month day_of_week year
0 9 * * * MON-FRI *Response: 201 Created
{
"id": "c1d2e3f4-...",
"next_fire_at": "2024-01-16T14:00:00Z"
}Get Cron Schedule
Response: 200 OK
Returns the full CronSchedule object.
List Cron Schedules
Response: 200 OK
Returns an array of CronSchedule objects.
Update Cron Schedule
Request Body (all fields optional)
{
"cron_expr": "0 10 * * * *",
"timezone": "Europe/London",
"enabled": false,
"metadata": { "type": "weekly-digest" }
}Response: 200 OK
Returns the updated CronSchedule.
Delete Cron Schedule
Response: 204 No Content
External Workers
Poll for Tasks
Request Body
{
"handler_name": "process_image",
"worker_id": "node-worker-42",
"limit": 10
}| Parameter | Type | Required | Description |
|---|---|---|---|
| handler_name | string | Yes | Handler to claim tasks for |
| worker_id | string | Yes | Unique worker identifier |
| limit | integer | No | Max tasks to claim. Default: 1 |
Response: 200 OK
[
{
"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.
FOR UPDATE SKIP LOCKED — concurrent workers never get the same task. Sets state = claimed, worker_id, claimed_at, and heartbeat_at.Complete Task
Request Body
{
"worker_id": "node-worker-42",
"output": {
"email_id": "msg-abc123",
"delivered": true
}
}| Parameter | Type | Required | Description |
|---|---|---|---|
| worker_id | string | Yes | Must match the worker that claimed the task |
| output | object | Yes | Result JSON (saved as BlockOutput) |
Response: 200 OK
Side effects
- 1. Worker task marked
completed - 2.
BlockOutputcreated with the output JSON - 3. Instance transitions
Waiting -> Scheduled(immediate re-processing) - 4. If instance uses execution tree, corresponding node marked
Completed
Fail Task
Request Body
{
"worker_id": "node-worker-42",
"message": "SMTP connection refused",
"retryable": true
}| Parameter | Type | Required | Description |
|---|---|---|---|
| worker_id | string | Yes | Must match claimer |
| message | string | Yes | Error description |
| retryable | boolean | No | Whether 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
Request Body
{
"worker_id": "node-worker-42"
}Response: 200 OK
Observability
Prometheus Metrics
Returns Prometheus text format (v0.0.4).
Counters
| Metric | Description |
|---|---|
| orch8_instances_claimed_total | Instances claimed by scheduler |
| orch8_instances_completed_total | Instances completed successfully |
| orch8_instances_failed_total | Instances failed (entered DLQ) |
| orch8_steps_executed_total | Steps executed |
| orch8_steps_failed_total | Steps that failed |
| orch8_steps_retried_total | Retry attempts |
| orch8_signals_delivered_total | Signals processed |
| orch8_rate_limits_exceeded_total | Rate limit deferrals |
| orch8_recovery_stale_instances_total | Stale instances recovered at startup |
| orch8_webhooks_sent_total | Webhooks delivered |
| orch8_webhooks_failed_total | Webhook delivery failures |
| orch8_cron_triggered_total | Cron instances created |
Histograms
| Metric | Description |
|---|---|
| orch8_tick_duration_seconds | Scheduler tick latency |
| orch8_step_duration_seconds | Individual step execution time |
| orch8_instance_processing_seconds | Total instance processing time |
Gauges
| Metric | Description |
|---|---|
| orch8_queue_depth | Instances claimed in current tick |
| orch8_active_tasks | Currently in-flight step executions |
Block Definitions
All blocks are defined in the blocks array of a sequence. Blocks can nest arbitrarily.
step
{
"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
| Handler | Params | Output |
|---|---|---|
| noop | (none) | {} |
| log | message (string), level ("debug"/"info"/"warn") | { "message": "..." } |
| sleep | duration_ms (integer, default 100) | { "slept_ms": N } |
| http_request | url, 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
{
"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
{
"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"
}| Semantics | Behavior |
|---|---|
| first_to_resolve | First branch to complete (success or failure) wins |
| first_to_succeed | First successful branch wins; failures ignored until all fail |
first_to_resolve is the default. Losing branches are cancelled.
router
{
"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
{
"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
{
"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
{
"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:
{
"error": "Human-readable error message"
}| Status Code | Meaning |
|---|---|
| 400 | Bad request (invalid JSON, missing required field, invalid state transition) |
| 404 | Resource not found (instance, sequence, cron schedule, worker task) |
| 409 | Conflict (state transition not allowed from current state) |
| 500 | Internal server error |