Built-in Handlers
The Orch8 engine ships with eighteen built-in step handlers covering HTTP integrations, AI orchestration, control flow, and state management. Custom handlers are registered in the handler registry; the built-ins below are always available.
HTTP & Integration
Handlers for making outbound HTTP and gRPC calls to external services, APIs, and tool endpoints.
http_request
Makes an HTTP request to any URL. Supports GET, POST, and PUT methods. The handler automatically retries on 5xx server errors using the step's retry configuration.
// GET request with custom headers
{
"type": "step",
"id": "fetch_user",
"handler": "http_request",
"params": {
"url": "https://api.example.com/users/{{context.data.user_id}}",
"method": "GET",
"headers": {
"Authorization": "Bearer {{context.config.api_token}}"
},
"timeout_ms": 10000
}
}
// POST request with a JSON body
{
"type": "step",
"id": "create_order",
"handler": "http_request",
"params": {
"url": "https://api.example.com/orders",
"method": "POST",
"body": {
"product_id": "{{context.data.product}}",
"quantity": 1
},
"headers": {
"Content-Type": "application/json"
}
}
}| Parameter | Type | Required | Description |
|---|---|---|---|
| url | string | Yes | The target URL. Supports template expressions. |
| method | string | No | HTTP method: GET, POST, or PUT. Defaults to GET. |
| body | object | No | Request body, serialised as JSON. |
| headers | object | No | Key-value map of HTTP headers. |
| timeout_ms | number | No | Request timeout in milliseconds. Defaults to 30000. |
tool_call
Sends a structured tool invocation to an HTTP endpoint. Designed for AI-agent tool-use patterns where the LLM returns a tool name and arguments, and a separate step executes the actual call.
{
"type": "step",
"id": "run_tool",
"handler": "tool_call",
"params": {
"url": "https://tools.internal/execute",
"tool_name": "search_database",
"arguments": {
"query": "{{steps.plan.output.search_query}}",
"limit": 10
},
"method": "POST",
"headers": {
"X-Tenant": "{{context.tenant_id}}"
},
"timeout_ms": 15000
}
}| Parameter | Type | Required | Description |
|---|---|---|---|
| url | string | Yes | The tool endpoint URL. |
| tool_name | string | No | Identifier for the tool being invoked. |
| arguments | object | No | Payload passed to the tool as its input. |
| method | string | No | HTTP method. Defaults to POST. |
| headers | object | No | Additional HTTP headers. |
| timeout_ms | number | No | Request timeout in milliseconds. Defaults to 30000. |
grpc:// (gRPC handlers)
Call any gRPC service as a workflow step by prefixing the handler with grpc://. The engine sends step params as JSON and receives JSON back. Retry, timeout, and circuit breaker all apply identically to gRPC handlers -- no worker polling is needed.
{
"type": "step",
"id": "classify",
"handler": "grpc://classifier:50051/Classifier.Predict",
"params": {
"text": "{{context.data.input}}"
},
"timeout": 30000,
"retry": {
"max_attempts": 2,
"initial_backoff": 1000,
"max_backoff": 5000,
"backoff_multiplier": 2.0
}
}The handler string follows the pattern grpc://host:port/Service.Method. The engine resolves the host via standard DNS, connects over HTTP/2, and transparently maps JSON params to the Protobuf request message.
AI & Agent
Handlers for calling language models, delegating decisions to humans, and letting agents modify their own execution plan at runtime.
llm_call
Built-in handler for calling language model APIs. Supports ten providers out of the box. The handler normalises the response into a consistent output shape regardless of provider.
{
"type": "step",
"id": "summarise",
"handler": "llm_call",
"params": {
"provider": "anthropic",
"model": "claude-sonnet-4-20250514",
"system_prompt": "You are a concise summariser.",
"prompt": "Summarise the following text:
{{context.data.article}}",
"temperature": 0.3,
"max_tokens": 512
}
}| Parameter | Type | Required | Description |
|---|---|---|---|
| provider | string | Yes | LLM provider. See supported list below. |
| model | string | Yes | Model identifier (provider-specific). |
| prompt | string | Yes | The user/input prompt sent to the model. |
| system_prompt | string | No | System-level instruction for the model. |
| temperature | number | No | Sampling temperature (0.0 - 2.0). Defaults to 1.0. |
| max_tokens | number | No | Maximum tokens in the response. |
Supported providers: openai, anthropic, gemini, deepseek, qwen, perplexity, groq, together, mistral, openrouter.
context.config, enable storage encryption and restrict handler context access.human_review
Pauses the running instance and waits for an external human signal before continuing. This is equivalent to wait_for_input but named for agent contexts where a human must approve, reject, or augment the agent's plan.
{
"type": "step",
"id": "approval",
"handler": "human_review",
"params": {
"prompt": "Review the generated report before publishing."
}
}send_signal call.self_modify
Injects new blocks into the running instance at runtime. An LLM or orchestration layer returns a list of tool calls as JSON; the handler inserts those as real execution steps. You can also inject blocks externally via the REST API.
// Step 1: LLM decides what to do next
{
"type": "step",
"id": "analyze_task",
"handler": "llm_call",
"params": {
"provider": "openai",
"model": "gpt-4o",
"prompt": "Return a JSON array of tool calls for: {{context.data.request}}"
}
}
// Step 2: Inject the LLM's tool calls as real steps
{
"type": "step",
"id": "inject_tools",
"handler": "self_modify",
"params": {
"blocks": "{{steps.analyze_task.output.tool_calls}}",
"position": 0
}
}
// External injection via REST API
// POST /instances/{id}/inject-blocks
// {
// "blocks": [{ "type": "step", "id": "new_step", "handler": "fetch_data", "params": { "url": "..." } }],
// "inject_after": "current"
// }| Parameter | Type | Required | Description |
|---|---|---|---|
| blocks | array | Yes | JSON array of block definitions to inject. |
| position | number | No | Insertion point: 0 = before the next unexecuted block. Omit to append at the end. |
Control Flow
Handlers for pausing execution, failing intentionally, asserting conditions, and coordinating between workflow instances.
sleep
Pauses execution for a specified duration. The engine reschedules the step rather than blocking a thread, so sleeping steps consume zero resources.
params.duration_ms belongs to the explicit sleep handler. A block's delay.duration is a scheduling delay before that block starts; it does not invoke this handler.
{
"type": "step",
"id": "wait_before_retry",
"handler": "sleep",
"params": {
"duration_ms": 5000
}
}| Parameter | Type | Required | Description |
|---|---|---|---|
| duration_ms | number | Yes | How long to sleep in milliseconds. |
noop
Does nothing and returns an empty object. Useful as a placeholder step, a delay-only step (when combined with a timeout), or a join point in complex control-flow graphs.
{
"type": "step",
"id": "checkpoint",
"handler": "noop"
}fail
Immediately fails the step with a custom error message. Use this to short-circuit execution when a precondition is not met, or to mark a branch as explicitly errored.
{
"type": "step",
"id": "reject_invalid",
"handler": "fail",
"params": {
"message": "Input validation failed: missing required field 'email'"
}
}| Parameter | Type | Required | Description |
|---|---|---|---|
| message | string | Yes | Error message attached to the step failure. |
assert
Evaluates a condition expression and fails the step if it resolves to false. Useful for guard clauses and data-quality checks within a workflow.
{
"type": "step",
"id": "check_balance",
"handler": "assert",
"params": {
"condition": "{{context.data.balance}} >= 100",
"message": "Insufficient balance: need at least 100, got {{context.data.balance}}"
}
}| Parameter | Type | Required | Description |
|---|---|---|---|
| condition | string | Yes | Expression that must evaluate to true. |
| message | string | No | Error message if the assertion fails. |
emit_event
Fires an event trigger to spawn a new workflow instance within the same tenant. Supports deduplication to prevent duplicate instances from identical events.
{
"type": "step",
"id": "notify_downstream",
"handler": "emit_event",
"params": {
"trigger_slug": "order_completed",
"dedupe_key": "{{context.data.order_id}}",
"dedupe_scope": "tenant"
}
}| Parameter | Type | Required | Description |
|---|---|---|---|
| trigger_slug | string | Yes | The slug of the trigger to fire. |
| dedupe_key | string | No | Deduplication key. If a matching event was already emitted, this one is skipped. |
| dedupe_scope | string | No | Scope for deduplication: "parent" (this instance) or "tenant" (tenant-wide). |
send_signal
Sends a signal to another running instance within the same tenant. Use this to resume paused instances, pass data between workflows, or coordinate multi-instance processes.
{
"type": "step",
"id": "resume_reviewer",
"handler": "send_signal",
"params": {
"instance_id": "{{context.data.reviewer_instance_id}}",
"signal_type": "review_complete",
"payload": {
"approved": true,
"reviewer": "{{context.data.user_email}}"
}
}
}| Parameter | Type | Required | Description |
|---|---|---|---|
| instance_id | string | Yes | ID of the target instance to signal. |
| signal_type | string | Yes | Signal type identifier the target instance is waiting for. |
| payload | object | No | Arbitrary data delivered with the signal. |
State Management
Handlers for reading, writing, and transforming data within an instance's execution context.
set_state / get_state / delete_state
Read, write, and remove values from session-scoped state. Session state persists for the lifetime of the instance and is accessible from any step via template expressions.
// Write a value
{
"type": "step",
"id": "save_token",
"handler": "set_state",
"params": {
"key": "auth_token",
"value": "{{steps.login.output.token}}"
}
}
// Read a value
{
"type": "step",
"id": "read_token",
"handler": "get_state",
"params": {
"key": "auth_token"
}
}
// Remove a value
{
"type": "step",
"id": "clear_token",
"handler": "delete_state",
"params": {
"key": "auth_token"
}
}| Parameter | Type | Required | Description |
|---|---|---|---|
| key | string | Yes | The state key to read, write, or delete. |
| value | any | set_state only | The value to store. Can be a string, number, object, or array. |
merge_state
Merges an object into context.data. Existing keys are overwritten, new keys are added. Useful for aggregating results from parallel branches or enriching context data.
{
"type": "step",
"id": "enrich_context",
"handler": "merge_state",
"params": {
"data": {
"enriched_at": "{{now}}",
"score": "{{steps.score_lead.output.score}}",
"tier": "{{steps.score_lead.output.tier}}"
}
}
}| Parameter | Type | Required | Description |
|---|---|---|---|
| data | object | Yes | Key-value map to merge into context.data. |
transform
Evaluates an expression against the current context and writes the result to a specified output key. Use this for data reshaping, filtering, or computed values between steps.
{
"type": "step",
"id": "extract_emails",
"handler": "transform",
"params": {
"expression": "context.data.users[*].email",
"output_key": "email_list"
}
}| Parameter | Type | Required | Description |
|---|---|---|---|
| expression | string | Yes | Expression evaluated against the current context. |
| output_key | string | Yes | Key where the result is stored in the step output. |
query_instance
Reads another instance's context and state within the same tenant. Useful for coordination patterns where one workflow needs to inspect the progress or output of another.
{
"type": "step",
"id": "check_parent",
"handler": "query_instance",
"params": {
"instance_id": "{{context.data.parent_instance_id}}"
}
}| Parameter | Type | Required | Description |
|---|---|---|---|
| instance_id | string | Yes | ID of the instance to query. Must belong to the same tenant. |
log
Emits a structured log entry. Logs are attached to the instance and visible in the dashboard and via the API.
{
"type": "step",
"id": "audit_entry",
"handler": "log",
"params": {
"message": "Order {{context.data.order_id}} processed successfully",
"level": "info"
}
}| Parameter | Type | Required | Description |
|---|---|---|---|
| message | string | Yes | The log message. Supports template expressions. |
| level | string | No | Log level: "debug", "info", or "warn". Defaults to "info". |
Step Execution & Retry
Every handler -- built-in or custom -- goes through the same execution pipeline. Understanding this flow helps you configure timeouts, retries, and error handling correctly.
Step execution flow
- ›Check for memoized output -- if the step already ran, return cached result
- ›Apply timeout (if configured) -- fail the step if it exceeds the limit
- ›Call the handler function with a StepContext (params, context, metadata)
- ›On success: persist output to block_outputs with size tracking
- ›On retryable error: calculate next backoff (initial_ms x multiplier^attempt, capped at max), reschedule
- ›On permanent error: mark step as failed, propagate up the execution tree
Retry & backoff
Each step can configure independent retry behavior. All retry fields are optional and can be set per-step in the workflow definition.
| Parameter | Type | Required | Description |
|---|---|---|---|
| max_attempts | number | No | Total attempts including the initial (e.g., 5 = 1 initial + 4 retries). |
| initial_backoff | string | No | Delay before the first retry (e.g., "1s"). |
| max_backoff | string | No | Ceiling on backoff growth (e.g., "60s"). |
| backoff_multiplier | number | No | Growth factor per attempt. Default: 2.0. |
{
"type": "step",
"id": "call_api",
"handler": "http_request",
"params": { "url": "https://api.example.com/data" },
"retry": {
"max_attempts": 5,
"initial_backoff": 1000,
"max_backoff": 60000,
"backoff_multiplier": 2.0
}
}Example progression with initial=1s, multiplier=2.0, max=60s:
Attempt 1: immediate
Attempt 2: wait 1s
Attempt 3: wait 2s
Attempt 4: wait 4s
Attempt 5: wait 8s
Attempt 6: wait 16s
Attempt 7: wait 32s
Attempt 8+: wait 60s <- capped at max_backoffmax_attempts values are safe. For non-idempotent writes, keep retries low and use assert steps to verify state before retrying.Handler Failure Catalogue
Classify the outcome before retrying. Retryability describes the handler result, not whether repeating an external effect is safe.
| Signal | Retry? | Typical cause | Action |
|---|---|---|---|
| timeout / network unavailable | Usually | Transient provider or network failure | Retry with backoff and a stable idempotency key. |
| HTTP 429 | Yes | Provider quota or rate window | Honor Retry-After and reduce concurrency. |
| HTTP 5xx | Usually | Remote service failure | Retry within budget; open the circuit after repeated failures. |
| HTTP 4xx | No | Bad auth, input, or missing resource | Fix input or credentials before a manual retry. |
| schema / assertion | No | Permanent contract mismatch | Inspect the saved input and deploy a corrected sequence version. |
| unknown external outcome | Reconcile first | Connection ended after request dispatch | Query the provider using the idempotency key before deciding. |
Ready to try Orch8?
One command to install. Then run your first local sequence.
curl -fsSL https://orch8.io/start.sh | sh