Skip to main content

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:

  1. The flow is Enabled in the connection's Business Flows list.
  2. The flow is Valid (no validation errors at sync time).
  3. Its current version is Accepted (first discovery auto-accepts).
  4. The conversation has no active run already (one run per conversation at a time).
  5. The flow uses the default conversation entry (no trigger, or trigger.type = conversation).
  6. The message matches the flow's intent — the Runtime embeds the customer message and compares it against an embedding of each flow's intent utterances + 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

StepRuntime behaviorRun status while waiting
askPrompts the customer; their next message is stored under fieldwaiting_for_input
toolCalls the referenced Business Tool via tool.invoke; output stored as a variablerunning (or waiting_for_challenge if auth is needed)
conditionEvaluates when against collected variables; jumps to then/else step id— (instant)
approvalNotifies the customer, then pauses until an Owner/Admin approves in Flow Runswaiting_for_approval
challengeExplicitly triggers customer verification (same machinery as auth: required tools)waiting_for_challenge
uploadAsks the customer for a filewaiting_for_upload
delaySleeps for duration_seconds (or until an ISO timestamp in until); a recovery worker wakes the runpaused
completeSends the final message and finishes the runcompleted

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:

  • ask stores the customer's answer under its field (e.g. order_id).
  • tool stores the tool's structured result under the tool's name (e.g. order_status_check), or under a custom output key 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:

  1. The customer receives the step's prompt ("your request has been sent to a supervisor…").
  2. The run shows in Flow Runs with a Needs approval badge and an inline Approve button.
  3. Any Owner or Admin of the organization can approve (Members cannot — the API enforces this). Approval resumes the run at the next step.
  4. 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 tool step references a tool declared with auth: 'required'. The Runtime invokes the tool, your customer provider's authorize returns a challenge (e.g. sms_otp), and the run pauses in waiting_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 challenge step, 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:

StatusMeaning
running / pendingActively executing steps
waiting_for_input / waiting_for_uploadWaiting on the customer
waiting_for_approvalWaiting on an Owner/Admin — approve inline or from the drawer
waiting_for_challengeWaiting on customer verification (e.g. OTP)
pausedSleeping in a delay step; auto-wakes
completed / failed / cancelledTerminal

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:

  1. Customer: "cancel my order" → intent matches, run starts, AI asks for the order ID.
  2. Customer: "ORD-1001" → stored as order_id; order_status_check runs; found == true branches to the approval step.
  3. Customer is told a supervisor must approve; run shows Needs approval in Flow Runs.
  4. An Owner/Admin clicks Approve → the run resumes; order_cancel requires auth, so the customer receives the OTP prompt on their channel.
  5. Customer replies with the code → the tool executes → "Order ORD-1001 has been cancelled…".

Workflow checklist

Ship an executable flow

  1. Declare + syncapp.flow(...) in your backend, then Sync Tools in Admin Console.
  2. Enable the flowValid + accepted flows can be toggled Enabled per connection.
  3. Tune intentWrite intent utterances as customers phrase them — they drive selection.
  4. Test end-to-endTrigger from chat; watch the run advance in Flow Runs.
  5. Approve + verifyExercise approval and challenge steps with a real Owner/Admin account.
  6. MonitorUse the run drawer timeline to debug failed or stuck runs; Retry after fixing.

FAQ

Does my backend need new code for flow execution?
No. Flows execute entirely in the Qefro Runtime. Your backend only answers the same signed tool.invoke calls it already handles for AI-initiated tool use — plus authorize/verify if your tools use challenges.
Who can approve an approval step?
Any Owner or Admin of the organization, from the Flow Runs page. Members are rejected by the API. There is no per-flow approver assignment today.
What happens if the customer walks away mid-flow?
The run stays in its waiting state (waiting_for_input, waiting_for_approval, …) and is visible in Flow Runs. An admin can cancel it at any time; a new message from the customer resumes it.
How does the runtime pick between multiple enabled flows?
Semantic similarity: the customer message is embedded and compared against each flow's intent utterances, name, and description. The highest-scoring flow above the threshold starts; below the threshold, the normal AI tool loop handles the message.
Why did my flow not trigger at all?
For conversation flows: Enabled, Valid, Accepted, no active run, and a message that resembles your intent utterances. For event/schedule/webhook flows: the bus event name must match trigger.event (namespaced), any when predicate must pass, and the event must not be expired — see Event-Driven Triggers.
Can the same flow start from both chat and a connector event?
Not with a single trigger. Entry is either conversation (intent) or event/schedule/webhook. Declare two flow ids if you need both entry styles for a related process.
Are approval and challenge messages delivered to WhatsApp?
Yes. When a run advances out-of-band (an admin approves, a delay wakes), the resulting message is persisted to the conversation and pushed to the customer's channel — WhatsApp customers receive it as a normal message.