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
Do not commit API keys in a sequence definition. Prefer a tenant-scoped credential or let an external worker read secrets from its deployment secret manager. If a secret must be supplied throughcontext.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.

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.

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.

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": 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_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.

Handler Failure Catalogue

Classify the outcome before retrying. Retryability describes the handler result, not whether repeating an external effect is safe.

SignalRetry?Typical causeAction
timeout / network unavailableUsuallyTransient provider or network failureRetry with backoff and a stable idempotency key.
HTTP 429YesProvider quota or rate windowHonor Retry-After and reduce concurrency.
HTTP 5xxUsuallyRemote service failureRetry within budget; open the circuit after repeated failures.
HTTP 4xxNoBad auth, input, or missing resourceFix input or credentials before a manual retry.
schema / assertionNoPermanent contract mismatchInspect the saved input and deploy a corrected sequence version.
unknown external outcomeReconcile firstConnection ended after request dispatchQuery the provider using the idempotency key before deciding.

Ready to try Orch8?

One command to install. Then run your first local sequence.

Bash
curl -fsSL https://orch8.io/start.sh | sh
Was this helpful?