Skip to main content

Chains & Workflows

Chains let you compose multi-step workflows where each step can be an HTTP webhook, a human approval gate, or a timed wait. Steps execute sequentially, pass data forward, and handle failures as a unit.

When to Use Chains

Use chains when you need steps that depend on each other. A chain shares context between steps -- the HTTP response from step 1 is available as a variable in step 3 -- and treats the whole sequence as a single unit for error handling.

Use individual actions when steps are independent and do not need shared state. For example, scheduling three unrelated webhooks is simpler as three separate actions.

FeatureIndividual ActionsChains
Steps depend on each otherNoYes
Data passed between stepsNoYes ({{steps.N.response.*}})
Fail/cancel as a unitNoYes
Human approval mid-workflowSeparate actionBuilt-in gate step

Step Types

TypeAPI Request ValueAPI Response AliasDescriptionUse For
HTTPhttp_callwebhookMakes an HTTP request and captures the responseAPI calls, webhooks, data processing
ApprovalgatedapprovalSends a message to recipients and waits for a human responseSign-offs, reviews, confirmations
WaitdelaywaitPauses the chain for a specified durationCooling periods, rate limiting, delays between steps

In API requests, use http_call, gated, delay. API responses return the aliases webhook, approval, wait.


Building a Chain

Here is a complete expense approval workflow with four steps: validate the expense, ask a manager to approve it, wait for a cooling period, then process the payment.

import { CallMeLater } from 'callmelater';

const client = new CallMeLater({ apiToken: 'sk_live_...' });

await client.chain('Expense Approval')
.input({ expense_id: 'exp_42', amount: 450.00, submitter: 'alice@example.com' })
.errorHandling('fail_chain')

// Step 0: Validate the expense
.addHttpStep('Validate Expense')
.url('https://api.example.com/expenses/validate')
.post()
.body({
expense_id: '{{input.expense_id}}',
amount: '{{input.amount}}'
})
.maxAttempts(3)
.done()

// Step 1: Manager approval
.addGateStep('Manager Approval')
.message('Approve expense of ${{input.amount}} submitted by {{input.submitter}}?')
.to('manager@example.com')
.timeout('48h')
.onTimeout('cancel')
.done()

// Step 2: Cooling period
.addDelayStep('Cooling Period')
.hours(1)
.done()

// Step 3: Process payment (only if approved)
.addHttpStep('Process Payment')
.url('https://api.example.com/payments')
.post()
.body({
expense_id: '{{input.expense_id}}',
validation_ref: '{{steps.0.response.reference}}'
})
.condition("{{steps.1.status}} == confirmed")
.done()

.send();

Variable Interpolation

Chains support {{...}} expressions in step URLs, headers, bodies, and gate messages. Variables are resolved at the moment each step executes, so later steps can reference results from earlier ones.

Input variables

Access values from the chain's input object:

{{input.expense_id}}     -> "exp_42"
{{input.amount}} -> 450.00
{{input.submitter}} -> "alice@example.com"

Previous step responses

Access the HTTP response body from a completed webhook step. Steps are zero-indexed:

{{steps.0.response.reference}}   -> value from step 0's JSON response
{{steps.0.response.data.id}} -> nested field access
{{steps.2.response.total}} -> value from step 2's response

Step status

Check what happened in a previous step:

{{steps.0.status}}   -> "executed", "failed", "skipped"
{{steps.1.status}} -> "confirmed", "declined" (for approval steps)

Status values by step type:

Step TypePossible Statuses
HTTP (webhook)executed, failed, skipped
Approvalconfirmed, declined, skipped
Waitexecuted, skipped

Conditions

Add a condition to any step to make it execute only when the expression evaluates to true. If the condition is not met, the step is marked as skipped and the chain continues.

Operators

OperatorDescriptionExample
==Equal{{steps.1.status}} == confirmed
!=Not equal{{steps.0.status}} != failed
containsString contains{{steps.0.response.role}} contains admin
not_containsString does not contain{{steps.0.response.tags}} not_contains deprecated
starts_withStarts with{{steps.0.response.env}} starts_with prod
ends_withEnds with{{steps.0.response.email}} ends_with @example.com
>Greater than{{steps.0.response.total}} > 1000
<Less than{{steps.0.response.priority}} < 3
>=Greater than or equal{{steps.0.response.score}} >= 80
<=Less than or equal{{steps.0.response.risk}} <= 5

Examples

// Only process payment if manager approved
{{steps.1.status}} == confirmed

// Skip notification for low-value orders
{{steps.0.response.total}} > 100

// Execute only if the validation response contains "approved"
{{steps.0.response.result}} == approved

Error Handling

Set the error_handling field on the chain to control what happens when a step fails.

fail_chain (default)

The entire chain stops immediately. No further steps execute. The chain status becomes failed.

{ "error_handling": "fail_chain" }

Use this for workflows where every step is critical -- if payment validation fails, you do not want to proceed to approval.

skip_step

The failed step is marked as skipped and the chain continues to the next step. Use this for workflows with optional steps.

{ "error_handling": "skip_step" }

Use this when some steps are nice-to-have. For example, a notification step that fails should not block a payment.

tip

Combine skip_step with conditions for fine-grained control. Even with skip_step enabled, you can use a condition on a later step to check whether a critical earlier step succeeded:

"condition": "{{steps.0.status}} == executed"

Chain Statuses

StatusDescription
pendingChain created, first step has not started
runningAt least one step has started executing
completedAll steps finished (executed, confirmed, or skipped)
failedA step failed and error_handling is fail_chain
cancelledChain was cancelled via the API

Managing Chains

// Get chain details
const chain = await client.getChain('chn_abc123');
console.log(chain.status, chain.current_step);

// List chains
const chains = await client.listChains({ status: 'running' });

// Cancel a chain (cancels all pending steps)
await client.cancelChain('chn_abc123');
note

Cancelling a chain cancels all pending steps. Steps that have already executed are not rolled back.