Workflows that pause for human input and resume when ready — built with iii-sdk.
This example shows how to build durable workflows that can pause indefinitely waiting for external signals (a user clicking a button, a manager approving an order) and resume exactly where they left off.
- Save state after each step as a checkpoint
- Workflow pauses by saving
awaiting_approvalstatus (no publish) - Webhook API resumes the flow when the external system calls it
No magic. Just state + API endpoints as re-entry points.
POST /orders → order::submit
↓ publish('order.submitted')
order::analyze-risk
↙ ↘
riskScore ≤ 70 riskScore > 70
↓ ↓
publish('order.auto_approved') PAUSE (save state, no publish)
↓ ↓
order::complete POST /webhooks/orders/:id/approve
↓
publish('order.approved')
↓
order::complete
Cron: order::detect-timeouts (every 5 min)
pnpm installConfigure the engine to run this worker by pointing to iii-config.yaml, then start the engine:
cargo run --releaseOr run the worker directly in dev mode:
bun run --watch src/main.tsSubmit a high-risk order (will pause):
curl -X POST http://localhost:3000/orders \
-H "Content-Type: application/json" \
-d '{
"items": [{"name": "Expensive Item", "price": 500, "quantity": 3}],
"customerEmail": "test@example.com",
"total": 1500
}'Response: {"orderId": "abc-123", "status": "pending_analysis", "message": "..."}
The workflow pauses at awaiting_approval — check state to confirm.
Resume it (minutes/hours/days later):
curl -X POST http://localhost:3000/webhooks/orders/abc-123/approve \
-H "Content-Type: application/json" \
-d '{
"approved": true,
"approvedBy": "manager@company.com",
"notes": "Verified"
}'The workflow continues — loads state, publishes order.approved, completes fulfillment.
./test-htl-flow.shsrc/
lib/iii.ts SDK init (registerWorker + Logger) and shared types
handlers/
submit-order.ts HTTP POST /orders — entry point
analyze-risk.ts Durable subscriber on 'order.submitted'
approval-webhook.ts HTTP POST /webhooks/orders/:orderId/approve — re-entry point
complete-order.ts Durable subscriber on 'order.approved' + 'order.auto_approved'
detect-timeouts.ts Cron — finds stuck orders every 5 minutes
main.ts Entry point — imports all handlers
Handlers call iii.trigger() directly with built-in function IDs — see the
iii state and
iii queue docs for the full list of operations.
if (riskScore > 70) {
order.status = 'awaiting_approval'
order.currentStep = 'awaiting_approval'
await iii.trigger({
function_id: 'state::set',
payload: { scope: 'orders', key: orderId, value: order },
})
// No publish — workflow stops here
}const order = await iii.trigger({
function_id: 'state::get',
payload: { scope: 'orders', key: orderId },
})
if (order.currentStep !== 'awaiting_approval') {
return { status_code: 400, body: { error: 'Not awaiting approval' } }
}
order.status = 'approved'
await iii.trigger({
function_id: 'state::set',
payload: { scope: 'orders', key: orderId, value: order },
})
await iii.trigger({
function_id: 'iii::durable::publish',
payload: { topic: 'order.approved', data: { orderId } },
})order::complete subscribes to both order.approved and order.auto_approved:
iii.registerTrigger({
type: 'durable:subscriber',
function_id: ref.id,
config: { topic: 'order.approved' },
})
iii.registerTrigger({
type: 'durable:subscriber',
function_id: ref.id,
config: { topic: 'order.auto_approved' },
}){
"id": "abc-123",
"status": "awaiting_approval",
"currentStep": "awaiting_approval",
"completedSteps": ["submitted", "risk_analysis"],
"riskScore": 85,
"requiresApproval": true,
"approvedBy": null,
"approvedAt": null,
"createdAt": "2026-01-05T10:00:00Z",
"updatedAt": "2026-01-05T10:00:02Z"
}This example was migrated from the Motia human-in-the-loop example. Key changes:
| Motia | iii-sdk |
|---|---|
motia package |
iii-sdk@0.11.0 |
export const config + export const handler |
iii.registerFunction() + iii.registerTrigger() |
state.set() / state.get() / state.getGroup() |
iii.trigger({ function_id: 'state::set' | 'state::get' | 'state::list', payload }) |
emit({ topic, data }) |
iii.trigger({ function_id: 'iii::durable::publish', payload: { topic, data } }) |
return { status: 200 } |
return { status_code: 200 } |
req.pathParams |
input.path_params |
cron: '*/5 * * * *' (5-field) |
expression: '0 */5 * * * * *' (7-field) |
| Noop step for visualization | Removed (no equivalent needed) |
motia dev |
bun run --watch src/main.ts |
motia build |
esbuild |
Apache-2.0