API Reference#
Curated REST API guide for the Orch8 Engine. The running server's generated OpenAPI document and Swagger UI are authoritative for exact fields, status codes, and routes. All product endpoints accept and return JSON; dates use ISO 8601 / RFC 3339 format.
Configurable via ORCH8_HTTP_ADDR. Bare product paths remain compatibility aliases, but new clients should use/api/v1. OpenAPI is served from/api-docs/openapi.json and Swagger UI from/swagger-ui. Send the same API-key and tenant headers used for product routes when the deployment protects these documentation routes; in insecure-auth mode a tenant header is still required by the current 0.7.0 middleware.
curl http://localhost:8080/api/v1/sequences -H "x-api-key: $ORCH8_API_KEY" -H "x-tenant-id: $ORCH8_TENANT_ID"API-key authentication is required unless the server is started with--insecure-auth. Tenant headers are required by default for authenticated tenant resources.
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": 1000,
"max_backoff": 60000,
"backoff_multiplier": 2.0
},
"timeout": 30000
},
{
"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": 3600000,
"business_days_only": false,
"jitter": 300000
},
"retry": {
"max_attempts": 3,
"initial_backoff": 1000,
"max_backoff": 60000,
"backoff_multiplier": 2.0
},
"timeout": 30000,
"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 |
Ready to try Orch8?
One command to install. Then run your first local sequence.
curl -fsSL https://orch8.io/start.sh | sh