Skip to content

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.

JSON
// 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"
    }
  }
}
ParameterTypeRequiredDescription
urlstringYesThe target URL. Supports template expressions.
methodstringNoHTTP method: GET, POST, or PUT. Defaults to GET.
bodyobjectNoRequest body, serialised as JSON.
headersobjectNoKey-value map of HTTP headers.
timeout_msnumberNoRequest timeout in milliseconds. Defaults to 30000.
Note
5xx responses are treated as retryable errors. 4xx responses fail the step immediately without retry.

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.

JSON
{
  "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
  }
}
ParameterTypeRequiredDescription
urlstringYesThe tool endpoint URL.
tool_namestringNoIdentifier for the tool being invoked.
argumentsobjectNoPayload passed to the tool as its input.
methodstringNoHTTP method. Defaults to POST.
headersobjectNoAdditional HTTP headers.
timeout_msnumberNoRequest 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.

JSON
{
  "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.

JSON
{
  "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
  }
}
ParameterTypeRequiredDescription
providerstringYesLLM provider. See supported list below.
modelstringYesModel identifier (provider-specific).
promptstringYesThe user/input prompt sent to the model.
system_promptstringNoSystem-level instruction for the model.
temperaturenumberNoSampling temperature (0.0 - 2.0). Defaults to 1.0.
max_tokensnumberNoMaximum tokens in the response.

Supported providers: openai, anthropic, gemini, deepseek, qwen, perplexity, groq, together, mistral, openrouter.

Warning
API keys should live in 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.

JSON
{
  "type": "step",
  "id": "approval",
  "handler": "human_review",
  "params": {
    "prompt": "Review the generated report before publishing."
  }
}
Tip
While waiting, the instance uses zero scheduler resources. It resumes instantly when the signal arrives via the REST API or a 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.

JSON
// 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"
// }
ParameterTypeRequiredDescription
blocksarrayYesJSON array of block definitions to inject.
positionnumberNoInsertion 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.

JSON
{
  "type": "step",
  "id": "wait_before_retry",
  "handler": "sleep",
  "params": {
    "duration_ms": 5000
  }
}
ParameterTypeRequiredDescription
duration_msnumberYesHow 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.

JSON
{
  "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.

JSON
{
  "type": "step",
  "id": "reject_invalid",
  "handler": "fail",
  "params": {
    "message": "Input validation failed: missing required field 'email'"
  }
}
ParameterTypeRequiredDescription
messagestringYesError 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.

JSON
{
  "type": "step",
  "id": "check_balance",
  "handler": "assert",
  "params": {
    "condition": "{{context.data.balance}} >= 100",
    "message": "Insufficient balance: need at least 100, got {{context.data.balance}}"
  }
}
ParameterTypeRequiredDescription
conditionstringYesExpression that must evaluate to true.
messagestringNoError 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.

JSON
{
  "type": "step",
  "id": "notify_downstream",
  "handler": "emit_event",
  "params": {
    "trigger_slug": "order_completed",
    "dedupe_key": "{{context.data.order_id}}",
    "dedupe_scope": "tenant"
  }
}
ParameterTypeRequiredDescription
trigger_slugstringYesThe slug of the trigger to fire.
dedupe_keystringNoDeduplication key. If a matching event was already emitted, this one is skipped.
dedupe_scopestringNoScope 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.

JSON
{
  "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}}"
    }
  }
}
ParameterTypeRequiredDescription
instance_idstringYesID of the target instance to signal.
signal_typestringYesSignal type identifier the target instance is waiting for.
payloadobjectNoArbitrary 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.

JSON
// 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"
  }
}
ParameterTypeRequiredDescription
keystringYesThe state key to read, write, or delete.
valueanyset_state onlyThe 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.

JSON
{
  "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}}"
    }
  }
}
ParameterTypeRequiredDescription
dataobjectYesKey-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.

JSON
{
  "type": "step",
  "id": "extract_emails",
  "handler": "transform",
  "params": {
    "expression": "context.data.users[*].email",
    "output_key": "email_list"
  }
}
ParameterTypeRequiredDescription
expressionstringYesExpression evaluated against the current context.
output_keystringYesKey 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.

JSON
{
  "type": "step",
  "id": "check_parent",
  "handler": "query_instance",
  "params": {
    "instance_id": "{{context.data.parent_instance_id}}"
  }
}
ParameterTypeRequiredDescription
instance_idstringYesID 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.

JSON
{
  "type": "step",
  "id": "audit_entry",
  "handler": "log",
  "params": {
    "message": "Order {{context.data.order_id}} processed successfully",
    "level": "info"
  }
}
ParameterTypeRequiredDescription
messagestringYesThe log message. Supports template expressions.
levelstringNoLog 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.

ParameterTypeRequiredDescription
max_attemptsnumberNoTotal attempts including the initial (e.g., 5 = 1 initial + 4 retries).
initial_backoffstringNoDelay before the first retry (e.g., "1s").
max_backoffstringNoCeiling on backoff growth (e.g., "60s").
backoff_multipliernumberNoGrowth factor per attempt. Default: 2.0.
JSON
{
  "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_backoff
Tip
For idempotent operations like GET requests, higher max_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.

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