What it does: a refund request is validated, then routed by amount — small refunds need one approval, large ones need a second (manager) approval. On approval it calls a payment connector and emails the customer.
When to use it: any policy-gated action with tiered authority — refunds, discounts, access grants, spend approvals.
Flow
validate ─▶ l1_approval (form) ─▶ after_l1
├─ rejected ───────────────▶ rejected
├─ amount > 500 ─▶ l2_approval (form) ─▶ after_l2 ─┐
└─ else ─────────────────────────────────────────▶ process_refund
│ onError → refund_failed
▼
notify_customer ─▶ end
The workflow
// Customer refund approval — threshold-based, multi-level human approval.
//
// A refund request is validated, then routed by amount: small refunds need one
// approval, large refunds need a second (manager) approval. On approval it calls a
// payment connector, then notifies the customer. Demonstrates branches, two forms,
// a connector, and a result reducer.
//
// Connections used: payments (generic-http), composio_gmail.
//
// flowctl def create --file examples/workflows/refund-approval.js
// flowctl run start --workflow RefundApproval \
// --input '{"order_id":"O-77","amount":800,"reason":"damaged"}' -q
// # then submit the approval form(s) the run parks on:
// flowctl form submit <exec> l1_approval --data '{"approved":true}'
function validate(ctx) {
return { status: "validated", needs_l2: ctx.vars.amount > 500 };
}
function on_l1(ctx, data) {
return { l1_approved: data.approved === true, l1_by: data.approver || "" };
}
function on_l2(ctx, data) {
return { l2_approved: data.approved === true, l2_by: data.approver || "" };
}
function decide(ctx) { return {}; }
function refund_req(ctx) {
return { body: { order_id: ctx.vars.order_id, amount: ctx.vars.amount, reason: ctx.vars.reason } };
}
function refund_resp(ctx, response) {
return { refund_id: response.body && response.body.id, refunded: response.status_code < 300, status: "refunded" };
}
function refund_failed(ctx) {
return { refunded: false, status: "refund_failed", error: ctx.error && ctx.error.message };
}
function notify_req(ctx) {
return { recipient_email: ctx.vars.customer_email, subject: "Refund for order " + ctx.vars.order_id, body: "Your refund has been processed." };
}
function notify_resp(ctx, response) { return { customer_notified: response.successful === true }; }
function rejected(ctx) { return { status: "rejected" }; }
workflow({
name: "RefundApproval",
version: "1",
description: "Threshold-based multi-level refund approval.",
initial: {
order_id: null, amount: 0, reason: "", customer_email: "customer@example.com",
l1_approved: false, l2_approved: false, needs_l2: false, status: "pending",
},
inputSchema: {
type: "object",
properties: { order_id: { type: "string" }, amount: { type: "number" }, reason: { type: "string" } },
required: ["order_id", "amount"],
},
triggers: [apiTrigger("request_refund")],
steps: [
step("validate", { start: true, next: "l1_approval", label: "Validate" }, validate),
form("l1_approval", {
title: "Approve refund for {{order_id}} ({{amount}})",
schema: {
type: "object",
properties: { approved: { type: "boolean" }, approver: { type: "string" } },
required: ["approved"],
},
next: "after_l1",
}, on_l1),
step("after_l1", {
branches: [
branch("l1_approved == false", "rejected"),
branch("needs_l2 == true", "l2_approval"),
branch("true", "process_refund"),
],
}, decide),
form("l2_approval", {
title: "Manager approval for {{order_id}} ({{amount}})",
schema: {
type: "object",
properties: { approved: { type: "boolean" }, approver: { type: "string" } },
required: ["approved"],
},
next: "after_l2",
}, on_l2),
step("after_l2", {
branches: [
branch("l2_approved == true", "process_refund"),
branch("true", "rejected"),
],
}, decide),
connector("process_refund", {
connection: "payments",
operation: "create_refund",
instance: "primary",
requestMapper: refund_req,
responseCombiner: refund_resp,
onError: "refund_failed",
next: "notify_customer",
}),
step("refund_failed", { next: "end", label: "Refund failed" }, refund_failed),
connector("notify_customer", {
connection: "composio_gmail",
operation: "send_email",
instance: "my-gmail",
requestMapper: notify_req,
responseCombiner: notify_resp,
next: "end",
}),
step("rejected", { next: "end", label: "Rejected" }, rejected),
],
outcomeTemplates: {
completed: "Refund {{order_id}} ({{amount}}) — {{status}}.",
},
result: (vars) => ({
order_id: vars.order_id,
amount: vars.amount,
status: vars.status,
refunded: vars.status === "refunded",
}),
});
Run it
flowctl def create --file examples/workflows/refund-approval.js
flowctl run start --workflow RefundApproval \
--input '{"order_id":"O-77","amount":800,"reason":"damaged"}' -q
flowctl form submit <exec> l1_approval --data '{"approved":true,"approver":"sam"}'
flowctl form submit <exec> l2_approval --data '{"approved":true,"approver":"manager"}' # only for amount > 500
Drive the forms from Claude Code
Start the run with —channel <id> and the approval forms are pushed into your
session, schema and all — see channels. Otherwise poll
run status and submit each form as the run parks on it.
Variations
- More tiers. Add an
amount > 5000branch and a third approval form. - Auto-approve small refunds. Route amounts under a floor straight to
process_refund. - Compensate on failure. Have
refund_failedopen a ticket or alert instead of just ending.