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, never in params. The engine reads keys from the config map at execution time, keeping secrets out of your workflow definitions.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.
{
"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": "1s",
"max_backoff": "60s",
"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.Ready to try Orch8?
One command to install. Two minutes to your first workflow.
curl -fsSL https://orch8.io/start.sh | sh