Run Business Flows
Define Business Flows covers declaring flows in your backend with app.flow(...). This guide covers what happens next: the Qefro Runtime executes those flows — asking questions, calling your Business Tools over the signed webhook, pausing for human approval, and verifying customers — while you monitor every run in Admin Console → Flow Runs.
Your backend stays passive. The Runtime drives the conversation and only calls back into your SDK when a tool step runs (a normal tool.invoke, identical to AI-initiated tool calls).
When a flow starts
Flows enter the Runtime through one of two paths. Both use the same FlowRunner (steps, tools, approvals, delays, challenges).
Conversation trigger (default)
A customer message starts a flow run only when all of these hold:
- The flow is Enabled in the connection's Business Flows list.
- The flow is Valid (no validation errors at sync time).
- Its current version is Accepted (first discovery auto-accepts).
- The conversation has no active run already (one run per conversation at a time).
- The flow uses the default conversation entry (no
trigger, ortrigger.type = conversation). - The message matches the flow's intent — the Runtime embeds the customer message and compares it against an embedding of each flow's
intentutterances + name + description. The best flow above a similarity threshold wins.
If no flow matches, the message is handled by the normal AI tool loop. Write intent utterances the way real customers phrase the request ("cancel my order", "stop my order from shipping") — they are the primary selection signal.
Event / schedule / webhook trigger
When metadata.trigger is event, schedule, or webhook, the flow does not start from chat intent. A producer emits a namespaced event into the org event bus; the dispatcher matches enabled flows and starts a system Channel::Event conversation + run. Payload fields become seed variables. Full guide: Event-Driven Triggers.
Step semantics at runtime
| Step | Runtime behavior | Run status while waiting |
|---|---|---|
ask | Prompts the customer; their next message is stored under field | waiting_for_input |
tool | Calls the referenced Business Tool via tool.invoke; output stored as a variable | running (or waiting_for_challenge if auth is needed) |
condition | Evaluates when against collected variables; jumps to then/else step id | — (instant) |
approval | Notifies the customer, then pauses until an Owner/Admin approves in Flow Runs | waiting_for_approval |
challenge | Explicitly triggers customer verification (same machinery as auth: required tools) | waiting_for_challenge |
upload | Asks the customer for a file | waiting_for_upload |
delay | Sleeps for duration_seconds (or until an ISO timestamp in until); a recovery worker wakes the run | paused |
complete | Sends the final message and finishes the run | completed |
Step prompts and completion messages are relayed conversationally: the Runtime lets the AI rephrase your prompt naturally (never inventing steps or values), falling back to your exact text if generation fails. Prompt Overrides set in Admin Console take precedence over the SDK-declared prompt.
Variables and templating
Every run carries a variables object:
askstores the customer's answer under itsfield(e.g.order_id).toolstores the tool's structured result under the tool's name (e.g.order_status_check), or under a customoutputkey if the step sets one.
Reference variables anywhere in prompts and messages with {{path}} templates, including dotted paths:
Order {{order_id}} is currently {{order_status_check.status}}.
By default a tool step receives all collected variables as its input, so a tool whose input_schema requires order_id finds the value collected by an earlier ask. To map explicitly, set input_map: { "tool_param": "variable.path" }.
Condition expressions
when is evaluated by a strict, non-eval parser:
expr := path OP literal | path exists | path empty | path
OP := == | != | >= | <= | > | <
literal := true | false | null | number | 'string' | "string" | bareword
- The left side is always a dotted variable path (
order_status_check.found). - Missing paths compare as
null; a bare path tests truthiness. - Examples:
order_status_check.found == true,order.total >= 100,customer.email exists,orders empty.
An unparseable expression fails the run; an expression that evaluates false with no else target simply falls through to the next step in the list.
Human approval
When a run reaches an approval step:
- The customer receives the step's prompt ("your request has been sent to a supervisor…").
- The run shows in Flow Runs with a Needs approval badge and an inline Approve button.
- Any Owner or Admin of the organization can approve (Members cannot — the API enforces this). Approval resumes the run at the next step.
- Whatever the flow does or says next is delivered back to the customer's channel — a WhatsApp customer gets a WhatsApp message even though they did not just send one; widget customers see it in their conversation history.
Cancelling the run from the drawer is the "reject" path — the run ends as cancelled and the customer can start over.
Customer verification (challenges)
Two ways a run verifies the customer:
- Implicit — a
toolstep references a tool declared withauth: 'required'. The Runtime invokes the tool, your customer provider'sauthorizereturns a challenge (e.g.sms_otp), and the run pauses inwaiting_for_challenge. The customer's answer resumes the suspended tool call — on success the tool executes with the verified customer and the flow continues. - Explicit — a standalone
challengestep, useful when you want verification before a branch rather than attached to one tool.
This is the same challenge machinery as regular authenticated tools — see Authentication and Challenge & Resume. Nothing extra to implement: if your tools already challenge, flows inherit it.
Monitor runs in Admin Console
Flow Runs (Business Tools section) lists every execution with live status:
| Status | Meaning |
|---|---|
running / pending | Actively executing steps |
waiting_for_input / waiting_for_upload | Waiting on the customer |
waiting_for_approval | Waiting on an Owner/Admin — approve inline or from the drawer |
waiting_for_challenge | Waiting on customer verification (e.g. OTP) |
paused | Sleeping in a delay step; auto-wakes |
completed / failed / cancelled | Terminal |
Opening a run shows the collected variables, current step, and a full event timeline (flow_started, step_completed, challenge_issued, approval_received, …). Failed runs can be retried from the last step; any non-terminal run can be cancelled.
Worked example
The cancel-order flow from examples/order-approval exercises everything above:
app
.flow({
id: 'cancel-order',
name: 'Cancel an order',
intent: ['cancel my order', 'i want to cancel an order', 'stop my order from shipping'],
category: 'orders',
version: 1,
})
.ask({
id: 'collect_order_id',
field: 'order_id',
prompt: 'Which order do you want to cancel? Please share your order ID.',
})
.tool({ id: 'lookup_order', tool_ref: 'order_status_check' })
.condition({
id: 'check_found',
when: 'order_status_check.found == true',
then: 'await_approval',
else: 'report_missing',
})
.complete({
id: 'report_missing',
message: "I couldn't find an order with ID {{order_id}}, so there is nothing to cancel.",
})
.approval({
id: 'await_approval',
prompt: 'Your cancellation request for order {{order_id}} has been sent to a supervisor for approval.',
})
.tool({ id: 'do_cancel', tool_ref: 'order_cancel' }) // auth: 'required' → OTP challenge
.complete({ id: 'confirm_cancelled', message: '{{order_cancel.message}}' });
What the customer and supervisor experience:
- Customer: "cancel my order" → intent matches, run starts, AI asks for the order ID.
- Customer: "ORD-1001" → stored as
order_id;order_status_checkruns;found == truebranches to the approval step. - Customer is told a supervisor must approve; run shows Needs approval in Flow Runs.
- An Owner/Admin clicks Approve → the run resumes;
order_cancelrequires auth, so the customer receives the OTP prompt on their channel. - Customer replies with the code → the tool executes → "Order ORD-1001 has been cancelled…".
Workflow checklist
Ship an executable flow
- Declare + sync — app.flow(...) in your backend, then Sync Tools in Admin Console.
- Enable the flow — Valid + accepted flows can be toggled Enabled per connection.
- Tune intent — Write intent utterances as customers phrase them — they drive selection.
- Test end-to-end — Trigger from chat; watch the run advance in Flow Runs.
- Approve + verify — Exercise approval and challenge steps with a real Owner/Admin account.
- Monitor — Use the run drawer timeline to debug failed or stuck runs; Retry after fixing.