Skip to main content

Define Business Flows

This guide advertises Business Flows from your backend using the Qefro Backend Framework (@qefro-ai/backend, qefro-backend-sdk, or qefro-backend). A flow describes how your existing Business Tools are orchestrated — which questions to ask, which tools to call, and in what order.

Flows are metadata only in your backend. The SDK advertises them through the capabilities.list message; the Qefro Runtime discovers, validates, versions, and executes them. Nothing runs inside the SDK. Register your tools first with Register SDK Business Tools — a flow references those tools by name. Once a flow is enabled, see Run Business Flows for conversation-driven execution, or Event-Driven Triggers to start the same Runtime from connectors, webhooks, and schedules.

Outcome

  • One or more flows declared with app.flow(...) in your backend
  • Flows advertised alongside tools via capabilities.list
  • Flows discovered on Sync Tools, validated, and shown in Admin Console → Business Tools → SDK Connections
  • First discovery auto-accepts; later changes surface an Accept / Reject version prompt

Prerequisites

  • A working SDK Connection with registered tools (see Register SDK Business Tools)
  • @qefro-ai/backend ≥ 1.1.0 or qefro-backend-sdk ≥ 1.2.0 or qefro-backend ≥ 1.0.0
  • Owner/Admin access in Admin Console

Concepts

TermMeaning
Flow{ metadata, steps } describing an orchestration of Business Tools
idImmutable identity of the flow. Renaming name never creates a new flow
Step{ id, type, config } — a single node; every step needs a unique id
triggerOptional entry path: conversation (default), event, schedule, or webhook — see Event-Driven Triggers
capabilities.listMessage that returns both tools and flows (supersedes tools.list)
Version detectionThe Runtime computes its own checksum over each flow and prompts to Accept/Reject changes
ValidationThe Runtime flags errors (block enabling) and warnings (informational) at sync time

Step 1 — Declare a flow

Chain step builders off app.flow(metadata). Each tool step references an existing tool by tool_ref.

TypeScript

import { Qefro } from '@qefro-ai/backend';

const app = new Qefro({ signingSecret: process.env.QEFRO_SIGNING_SECRET! });

// ...register app.tool('lookup_customer', ...) and app.tool('get_orders', ...) first

app
.flow({
id: 'order_lookup', // immutable identity — renaming `name` never creates a new flow
name: 'Order Lookup',
description: 'Lookup customer orders',
version: 1,
category: 'crm',
tags: ['customer', 'orders'],
intent: ['track order', 'where is my order', 'find my shipment'],
inputs: ['email'],
outputs: ['customer', 'orders'],
})
.ask({ id: 'email', field: 'email', prompt: 'Please enter your email.' })
.tool({ id: 'lookup', tool_ref: 'lookup_customer' })
.tool({ id: 'orders', tool_ref: 'get_orders' })
.complete({ id: 'done', message: 'Here are your recent orders.' });

await app.listen({ port: 8088, path: '/qefro' });

Rust

use qefro_backend_sdk::BusinessFlowMetadata;

app.flow(BusinessFlowMetadata {
id: "order_lookup".into(),
name: Some("Order Lookup".into()),
description: Some("Lookup customer orders".into()),
category: Some("crm".into()),
tags: vec!["customer".into(), "orders".into()],
intent: vec!["track order".into(), "where is my order".into()],
inputs: vec!["email".into()],
outputs: vec!["customer".into(), "orders".into()],
..Default::default()
})?
.ask("email", "email", "Please enter your email.")
.tool("lookup", "lookup_customer")
.tool("orders", "get_orders")
.complete("done", Some("Here are your recent orders.".into()))?;

In Rust, flow(...) and .complete(...) return Result — a duplicate or empty id (flow or step) surfaces a FlowError and the flow is excluded from capabilities.list instead of panicking. In TypeScript, the same mistakes throw at startup.

Python

from qefro_backend import Qefro

app = Qefro(signing_secret="...") # or QEFRO_SIGNING_SECRET from env

# ...register app.tool("lookup_customer", ...) and app.tool("get_orders", ...) first

(
app.flow({
"id": "order_lookup", # immutable identity — renaming `name` never creates a new flow
"name": "Order Lookup",
"description": "Lookup customer orders",
"category": "crm",
"tags": ["customer", "orders"],
"intent": ["track order", "where is my order", "find my shipment"],
"inputs": ["email"],
"outputs": ["customer", "orders"],
})
.ask("email", field="email", prompt="Please enter your email.")
.tool("lookup", tool_ref="lookup_customer")
.tool("orders", tool_ref="get_orders")
.complete("done", message="Here are your recent orders.")
)

app.run(8088) # POST http://0.0.0.0:8088/qefro

In Python, a duplicate or empty id (flow or step) raises FlowError at declaration time. Use the else_ argument on .condition(...) (Python keyword) — it is serialized as the else wire key.

Optional entry trigger

By default a flow starts from a matching customer message (intent). To start from a bus event, schedule, or webhook instead, set trigger on the flow metadata:

app.flow({
id: 'abandoned_cart_recovery',
name: 'Abandoned cart recovery',
trigger: { type: 'event', event: 'shopify.cart.abandoned' }, // namespaced
// trigger: { type: 'schedule', cron: '0 2 * * *' },
// trigger: { type: 'event', event: 'shopify.order.created', when: 'total >= 1000' },
})
.tool({ id: 'remind', tool_ref: 'send_cart_reminder' })
.complete({ id: 'done' });

Event names must be namespaced (shopify.cart.abandoned, not cart_abandoned). Full emit, ordering, replay, and connector guidance: Event-Driven Triggers.

Step 2 — Step types

Every step is { id, type, config }. id must be unique within the flow. Type-specific settings live in config, so future options extend config without breaking the wire format.

TypePurposeKey config
askCollect input from the customerfield, prompt
toolCall a registered Business Tooltool_ref
challengeTrigger a customer auth challengemessage?
uploadRequest a filefield?, prompt?, accept?
conditionBranch to other stepswhen, then?, else?
delayWait before continuingduration_seconds (or ISO until)
approvalRequire human approvalprompt?
completeTerminal stepmessage?

Step 3 — Sync and review in Admin Console

  1. Open Business Tools → SDK Connections and select a workspace.
  2. Click Sync Tools. The Runtime calls capabilities.list, imports tools as before, and discovers your flows.
  3. Each connection shows a Business Flows section grouped by category with a Valid / Invalid badge, version info, tags, and inputs/outputs.
  4. Toggle Enabled per flow (disabled for invalid flows), and edit Prompt Overrides for ask steps.

An enabled, valid, accepted flow is live: matching customer messages start a run the Runtime executes end to end — see Run Business Flows.

New flows are auto-accepted on first discovery. When you change a flow and re-sync, the Runtime detects the change (via its own checksum) and shows a New Flow Version Available banner with Accept and Reject. Accept promotes the pending version; Reject keeps the current one.

Versioning and identity

  • The Runtime owns change detection — it computes a SHA-256 over its own canonical serialization of each received flow. The SDK never computes checksums.
  • Identity is the immutable id. Rename name freely: the flow updates in place, no new version.
  • Bump version for your own record-keeping; the Runtime still uses its checksum to decide when an Accept/Reject prompt appears.

Protocol reference (what Qefro sends)

MessageWhenYour response
pingTest Connection{ "type": "pong", ... }
capabilities.listSync Tools{ "type": "capabilities.list", "tools": [...], "flows": [...] }
tools.listLegacy Sync (tools only){ "type": "tools.list", "tools": [...] }

The framework verifies and signs requests for you when you use app.listen().

Workflow checklist

Business Flows launch

  1. Register toolsapp.tool for every tool_ref your flow references.
  2. Declare flowsapp.flow(metadata) + step builders; unique step ids.
  3. Sync ToolsRuntime calls capabilities.list and discovers flows.
  4. Review validationFix any Invalid flows (errors block enabling).
  5. Enable + tuneToggle enabled; set ask-step prompt overrides.
  6. Accept changesOn later syncs, Accept/Reject new flow versions.

FAQ

Do flows execute inside my backend?
No. Flows are metadata only from the SDK's point of view: it advertises them through capabilities.list, and the Qefro Runtime stores, displays, and executes them. During execution your backend only answers normal tool.invoke calls for tool steps. Your tools behave exactly as before.
What happens if I only upgrade one side?
capabilities.list is additive on protocol version 1. An old SDK answers with an error and the Runtime falls back to tools.list (tools only, no flows). Upgrade the SDK to advertise flows.
How does the Runtime detect a changed flow?
It computes its own SHA-256 checksum over each received flow. First discovery auto-accepts; a differing checksum shows an Accept/Reject prompt. The SDK never computes checksums.
Can I rename a flow without losing its settings?
Yes. Identity is the immutable id. Renaming name updates the flow in place — enabled state and prompt overrides are preserved.
Why is my flow marked Invalid?
Common causes: a tool_ref that no tool advertises, a duplicate or missing step id, an unknown step type, no complete step, or a condition pointing at a nonexistent step. Fix these and re-sync; invalid flows cannot be enabled.