Anatomy of a workflow agent
Every recipe in this section follows the same pattern — pick a workflow, create the agent, ground it with knowledge, test it, and go live. Read this page once and the recipes themselves become short. Come back here whenever you need a refresher on an individual step.
1. Find or create a workflow
Agents are built on workflows. A workflow encodes the conversation logic — branching, handoff rules, and system prompt configuration. Before you create an agent you pick the workflow it will run: one you have built yourself (scope: "owned"), one from the public library (scope: "public"), or a brand-new one you create here.
import { BimpeAI } from "@bimpeai/sdk";
const bimpe = new BimpeAI({ apiKey: process.env.BIMPEAI_API_KEY! });
// Search the public library for a restaurant workflow
const page = await bimpe.workflows.list({ scope: "public", search: "restaurant" });
const workflow = page.data[0];
console.log(workflow.id, workflow.name);import os
from bimpeai import BimpeAI
client = BimpeAI(api_key=os.environ["BIMPEAI_API_KEY"])
# Search the public library for a restaurant workflow
page = client.workflows.list(scope="public", search="restaurant")
workflow = page.data[0]
print(workflow.id, workflow.name)scope accepts "owned", "public", or "accessible". list returns full Workflow objects carrying id, name, description, system_prompt, rules, flows, tags, guide, faq, setup_steps, and timestamps; workflows.retrieve(id) returns the same shape.
To build a workflow from scratch instead of picking one, call workflows.create. Only name and system_prompt are required; you can also seed rules, flows, tags, and the rest. It returns the new Workflow, and its id is what you bind the agent to in the next step. To start from an existing workflow, workflows.clone (passing the source workflow's id) copies it and returns the copy.
const workflow = await bimpe.workflows.create({
name: "Restaurant assistant",
system_prompt:
"You help diners browse the menu, filter by dietary needs, book tables, and place takeout orders.",
});
console.log(workflow.id);workflow = client.workflows.create(
name="Restaurant assistant",
system_prompt=(
"You help diners browse the menu, filter by dietary needs, "
"book tables, and place takeout orders."
),
)
print(workflow.id)2. Create an agent
With a workflow id in hand, create the agent. workflow_id wires the workflow, and name and description are required. The system prompt and rules are not agent-create fields — they live on the workflow. You can optionally supply a persona (enum), language, and timezone at create time or update them later.
const agent = await bimpe.agents.create(
{
name: "Restaurant assistant",
description: "Helps customers place orders and book tables.",
workflow_id: workflow.id,
},
{ idempotencyKey: "create-restaurant-agent-v1" },
);
console.log(agent.id);agent = client.agents.create(
name="Restaurant assistant",
description="Helps customers place orders and book tables.",
workflow_id=workflow.id,
idempotency_key="create-restaurant-agent-v1",
)
print(agent.id)Pass an idempotencyKey / idempotency_key so that a retried create cannot produce a duplicate agent.
3. Add a knowledge base (optional)
This step is optional. Add a knowledge base when the agent needs to quote accurate static content — a menu, a policy doc, an FAQ; skip it when the workflow's system prompt and your integrations already cover everything the agent says. Each entry is either a text source (inline content) or a url source (the SDK fetches and indexes the page).
// Inline text
await bimpe.agents.knowledgeBases.create(agent.id, {
type: "text",
name: "Menu",
content: "Margherita pizza £12. Pasta Arrabbiata £10. Tiramisu £6.",
});
// URL source
await bimpe.agents.knowledgeBases.create(agent.id, {
type: "url",
name: "Booking policy",
url: "https://example.com/booking-policy",
});# Inline text
client.agents.knowledge_bases.create(agent.id, {
"type": "text",
"name": "Menu",
"content": "Margherita pizza £12. Pasta Arrabbiata £10. Tiramisu £6.",
})
# URL source
client.agents.knowledge_bases.create(agent.id, {
"type": "url",
"name": "Booking policy",
"url": "https://example.com/booking-policy",
})Knowledge bases support full CRUD (list, create, update, delete). The agent picks them up immediately after creation.
4. Inspect agent sub-resources
Channels are read-only through the API — list them to see what is connected, but connect them on the Deploy screen of the Console dashboard. Integrations are listable and now also writable; see Configuring integrations. Actions are listable and can also be toggled with agents.actions.enable(agentId, { action_ids }) and disable(...). Conversation flows live on the workflow — inspect them with workflows.retrieve(workflow.id).flows.
const integrations = await bimpe.agents.integrations.list(agent.id);
const channels = await bimpe.agents.channels.list(agent.id);
const actions = await bimpe.agents.actions.list(agent.id);
console.log("Channels:", channels.map((c) => c.type));integrations = client.agents.integrations.list(agent.id)
channels = client.agents.channels.list(agent.id)
actions = client.agents.actions.list(agent.id)
print("Channels:", [c.type for c in channels])API boundary: what the API can and cannot do
Channels are read-only in the Console API. You connect a channel — WhatsApp, web chat, telephony/voice — on the Deploy screen of the Console dashboard (under Messaging & Chat and Voice); telephony numbers are provisioned and linked to an agent under Team settings → Phone numbers, with the agent's voice and greeting set under Settings → Voice. The API lists what is connected but cannot create or remove a channel connection. Integrations, by contrast, can be configured through the API — see Configuring integrations.
When filtering conversations by channel, the accepted values are whatsapp, webchat, and telephony, plus the test_* variant of each (test_whatsapp, test_webchat, test_telephony). Voice conversations are surfaced under the telephony channel, which is filterable.
5. Test your agent
Before going live, exercise the agent on a test channel. Fetch the test code with agents.getTestCode (it is created on first request), which also returns the per-channel deep links a tester uses. From there you test one of two ways: a human tester messages BimpeAI's number with the code (see the channel notes below), or you inject a message programmatically with conversations.send and is_test_channel: true. With role omitted the message defaults to user — a customer message the live agent replies to.
For step-by-step connect and test instructions per channel (WhatsApp, Instagram, Messenger, Web Chat, Web Voice, telephony), see Deploying and testing channels.
const { code, channels } = await bimpe.agents.getTestCode(agent.id);
console.log("Test code:", code, "WhatsApp link:", channels.whatsapp.url);
// Send a test message on a test channel (WhatsApp here).
await bimpe.conversations.send(agent.id, {
message: "Two for 7 pm tonight?",
channel_type: "whatsapp",
channel_user_id: "<tester-whatsapp-number>",
is_test_channel: true,
});test_code = client.agents.get_test_code(agent.id)
print("Test code:", test_code.code, "WhatsApp link:", test_code.channels.whatsapp.url)
# Send a test message on a test channel (WhatsApp here).
client.conversations.send(
agent.id,
message="Two for 7 pm tonight?",
channel_type="whatsapp",
channel_user_id="<tester-whatsapp-number>",
is_test_channel=True,
)The two paths support different channels:
- Human tester, via the test code —
getTestCodereturns a ready-to-sendstart_messageand a deep-linkurlfor WhatsApp, Instagram, and Messenger, along with the destination to send to (phone_number,username, orpage). The tester opens theurlor sends thestart_message, then chats with the agent. WhatsApp opens a 24-hour reply window once that first message lands; outside it, WhatsApp will not deliver agent messages. - Programmatic injection, via
conversations.sendwithis_test_channel: true— supported onchannel_typewhatsapp,webchat, andtelephony(surfaced astest_whatsapp,test_webchat,test_telephony). On telephony this places an outbound call rather than delivering text, and only succeeds when a telephony session is available.
Web voice and the in-app SDK widgets are not test channels.
Pause AI before you reply as the assistant
While AI chat is live (is_ai_chat_paused is false) you cannot post a message with role: "assistant" — the agent owns the reply. To hand a conversation to a human, pause AI on that conversation first, then send.
await bimpe.conversations.setAiStatus(agent.id, conversationId, { is_ai_chat_paused: true });client.conversations.set_ai_status(agent.id, conversation_id, is_ai_chat_paused=True)6. Go live
When the agent passes testing, switch it from development to live. The status is one of development, live, or paused. Confirm channels are connected on the Deploy screen before flipping live — see the go-live checklist.
await bimpe.agents.updateLiveStatus(agent.id, { status: "live" });client.agents.update_live_status(agent.id, status="live")7. Converse
Once an agent is live and channels are connected, conversations flow in automatically. You can send a message into an existing conversation from your server, or stream all new messages in real time.
// Send a message into a conversation
const sent = await bimpe.conversations.messages.send(agent.id, conversationId, {
message: "Your table for two is confirmed for 7 pm tonight.",
});
// Stream new messages in real time
const controller = new AbortController();
for await (const event of bimpe.conversations.messages.stream(agent.id, conversationId, {
signal: controller.signal,
})) {
console.log(event.role, event.message);
}# Send a message into a conversation
sent = client.conversations.messages.send(
agent.id, conversation_id, message="Your table for two is confirmed for 7 pm tonight."
)
# Stream new messages in real time
for event in client.conversations.messages.stream(agent.id, conversation_id):
print(event.role, event.message)The stream runs over Server-Sent Events. The SDK handles the ticket exchange, reconnects on dropped connections, and resumes from the last delivered message id — you never miss a message or see one twice. Break out of the loop (or abort the signal in TypeScript) to stop.
Every recipe in the Use cases section links back to this page and assumes you have read it. The recipes themselves focus on the domain-specific details: which workflow to pick, what goes in the knowledge base, and how the conversation flow is structured for that particular scenario.