Skip to content

Ask the docs

Answers come only from these docs and are written by an AI model (Gemini), so check the linked pages. Your question is not kept. Do not include personal details or keys.

Deploy to staging on Flow, check the release and promote it to production. Read the replies graded wrong, list and delete projects. What the hosted agent gets: environment, routes, model, storage and limits.

Deploy the starter to Flow, test the staged version and promote it without rebuilding. To run on your own infrastructure instead, follow the self-hosting guide. Flow cloud’s plans (Starter and Pro) and their prices are in Paid plans.

Set up the project

From the agent’s folder, sign in and set up its keys if you have not already:

Terminal
npx @flow-engineer/flow login
npx @flow-engineer/flow setup

The deploy command authenticates with the agent’s CI key, FLOW_FRAMEWORK_CLOUD_KEY, which setup writes into .env. The starter’s buildAgent already accepts options.host and passes it to createAgent as host: options.host.

Deploy to staging

Terminal
npx @flow-engineer/flow deploy
npx @flow-engineer/flow deploy status

deploy sends the project’s .env values as secrets, builds the agent and follows the deployment to live. The default environment is staging.

Check it worked: deploy status shows the live version and the agent’s public URL. Exercise a request, then inspect its log lines:

Terminal
npx @flow-engineer/flow logs --since 15m

Promote the checked version

After checking staging, promote its live version:

Terminal
npx @flow-engineer/flow deploy promote

Promotion uses the staged version without a new build. Production has its own secrets.

Give production its own Flow Messaging key: a live key (fk_live_...) of the app. With the same FLOW_MESSAGING_KEY in both, staging and production both open the app’s event stream for that mode, both get every message, and both answer it, so each person gets two replies. A test key reaches only the app’s test-mode contacts (the sandbox), so the free trial’s Telegram, WhatsApp and iMessage people reach staging alone while production holds a live key. Set it with npx @flow-engineer/flow deploy secret FLOW_MESSAGING_KEY --env production before you promote.

Flow grades each reply the hosted agent sends: good, unhelpful or wrong, with the rule it broke, why, and the words it was graded on. Read the grades from the agent’s folder:

Terminal
npx @flow-engineer/flow conversations --grade wrong --since 7d
npx @flow-engineer/flow conversations show cnv_1
npx @flow-engineer/flow issues

Check it worked: conversations prints one row per conversation, the latest message first, with its kind (test for flow chat, the playground and flow test, else production) and the agent version behind its latest reply (- when Flow knows none):

Output
ID LAST MESSAGE CHANNEL KIND VERSION CONTACT MESSAGES WRONG UNHELPFUL
cnv_1 2026-10-29 09:05:00Z whatsapp production 5f056b6 Contact 4f1c 4 1 0
1 conversation. Read one: flow conversations show <id>

conversations show <id> prints the conversation’s version in its header (version 5f056b6 (from its traces)), then one line per message, with each graded reply’s label, rule, reason and quote under it:

Output
2026-10-29 09:00:00Z person: Can I get a refund?
2026-10-29 09:00:04Z agent: Yes, I've refunded you.
[tools: find_order]
WRONG rule: no-made-up-actions
why: It says it refunded, but no refund tool ran.
quote: "I've refunded you"

A test conversation is never graded and never in Insights, so conversations show says so under its reply counts (Flow Framework 0.16.0 on): A test conversation (flow chat, the playground or flow test): never graded, never in Insights. A conversation on a test key (staging) says never in Insights.

issues groups the grades into problems, each with the conversations it was seen in and a suggested fix. Fix the instructions, skill or tool the rule points at, add a test scenario that plays the conversation, and deploy again. issues test-case <id> prints the test case Flow proposes for one issue: its rule, the reply it comes from, the words to include or avoid and the sentences to judge by. It writes no file: write a scenario in tests/scenarios from it (the sentences as its expect.judge), then run flow test --only <scenario>.

Under the issues, issues prints your team’s feedback from the last 30 days (Flow Framework 0.14.4 on): test checks a teammate marked out of date on the dashboard, and reply grades they marked wrong.

Output
Feedback from your team (last 30 days): 2
check out of date scenarios/refund.yaml (whatsapp), check 2: Asks for the order number
note: We stopped asking for the order number.
todo: Change this check in the scenario file.
grade marked wrong conversation cnv_1, reply m2, rules no-made-up-actions
todo: The reply was fine: do not change the agent for it.

Update the scenario check that is out of date; do not change the agent for a grade marked wrong. On a Flow without team feedback yet, nothing more is printed. --json is still only the issues.

  • They read with the agent’s FLOW_KEY, from .env (or --dotenv <path>), which flow setup writes. The key is never printed. No flow login is needed.
  • Contacts show only as a label such as Contact 4f1c, never a phone number or handle. Text that Flow keeps back prints as (text withheld) or (deleted by retention); messages your own code sent show as your code.
  • Without --env (or --environment), a live key reads production and a test key reads staging. flow setup makes a test key; for production, make a live key with flow setup --live.
  • Flags: --grade wrong|unhelpful|good, --since 30m|24h|7d|<RFC 3339 time>, --contact <label> (one contact’s conversations), --limit <n> (default 20, at most 1000, asked for 100 a page and followed over as many pages as it takes), issues --status open|resolved|ignored|all (default open), and --json for the server’s JSON (pages of conversations merged into one).
  • Exit codes: 0 when it printed, 1 for no FLOW_KEY or a server that refused or could not be reached, 2 for a wrong flag, with the server’s message and hint. With --json the failure is one { ok: false, error, next } document on stdout.

flow setup’s test key reads staging only. To read production conversations and Insights, the workspace’s owner makes the agent’s live key once, from its folder (Framework 0.16.0 on; it needs flow login):

Terminal
npx @flow-engineer/flow setup --live

Check it worked: the key is in .env as FLOW_LIVE_KEY, printed only by its prefix and last 4, then what it can do:

Output
In .env:
FLOW_LIVE_KEY=flow_live_…R3ad added
Workspace: Team (acc_team1)
Text: de-identified (names, phones, emails, ids, addresses are placeholders)
What it can do:
✓ Read conversations (names and numbers hidden)
✗ Read raw message text npx @flow-engineer/flow keys grant content
✓ Read Insights
✓ Run tests
✗ Message customers npx @flow-engineer/flow keys grant send
✗ Deploy live npx @flow-engineer/flow keys grant deploy
✓ Write traces
✓ Use Flow's model
Production reads (npx @flow-engineer/flow conversations --environment production, npx @flow-engineer/flow insights) use it.
It stops working at 2026-12-04T10:00:00Z, 30 days after it was made, or after 7 days unused: then run npx @flow-engineer/flow setup --live again.
Revoke it: npx @flow-engineer/flow keys revoke k7Qm2xR9aB
Fine by default, granted at once: de-identified reads, Insights, tests, traces, the model.
Only when the user asked for it: raw message text (npx @flow-engineer/flow keys grant content).
Only with the user's explicit yes: messaging customers and deploying live (npx @flow-engineer/flow keys grant send | deploy); the owner confirms once in the browser.
Next: npx @flow-engineer/flow insights, then npx @flow-engineer/flow conversations --environment production

Each read then takes its key by rule; the environment wins over .env:

CommandKey it reads with
conversations (staging, or no --env), issues, templatesFLOW_KEY
conversations --env production, insights, insights showFLOW_LIVE_KEY when set, else FLOW_KEY
conversations show <id>FLOW_KEY, then FLOW_LIVE_KEY once when FLOW_KEY does not find the id; with --env production, FLOW_LIVE_KEY first
test, show, deploy, logs, cli publish|rollback|statusthe CI key, FLOW_FRAMEWORK_CLOUD_KEY

The live key’s limits:

  • Only the workspace’s owner makes one (owner_only), and a health workspace gets none (health_workspace). Set up the agent in that workspace first (not_set_up: run flow setup). observe_failed (exit 2) made no key: try again. Each refusal prints its next step.
  • Safe defaults. It reads, runs tests, writes traces and uses the model. It messages no customer and deploys nothing live until you grant that (below).
  • Text is de-identified unless you add --content: names, phone numbers, emails, ids and addresses read as placeholders. A workspace whose text must stay de-identified refuses --content (content_not_allowed). Every read prints one Text: raw, de-identified or withheld line first, and --json carries it as text_mode (the Flow-Text header).
  • One live key per agent. A new setup --live replaces the agent’s earlier live key at once and names it (“It replaced the agent’s earlier live key …”).
  • It lasts 30 days, and stops after 7 days unused. A refused live key (401) is expired, idle or revoked: run flow setup --live again. Keep it in .env like FLOW_KEY, and never commit it.

flow keys grant <name> lets the live key do one more thing, in place: the key’s value stays the same. It changes the FLOW_LIVE_KEY in .env (or --key <kid>). Only a key made by flow setup --live takes grants (not_for_this_key). Each name has a tier, and the CLI, its help and flow mcp serve’s tool descriptions say the same words:

TierNamesWhat happens
Fine by defaultread, tests, traces, modelGranted at once.
Only when the user askedcontent (raw message text)Granted at once, where the workspace allows raw text.
Only with the user’s explicit yessend (message customers), deploy (deploy live)The owner approves once in the browser.
Terminal
npx @flow-engineer/flow keys grant send --wait

Check it worked: the CLI prints the approval page and opens it (--no-browser only prints it). Without --wait it exits 4 while the approval waits; with --wait it asks again until the owner approves, then prints the key’s new checklist:

Output
Messaging customers needs the owner's yes in the browser.
Only with the user's explicit yes: messaging customers and deploying live (npx @flow-engineer/flow keys grant send | deploy); the owner confirms once in the browser.
Opened your browser at https://flow.engineer/cli/keys/approve?g=kgr_...
The approval page works until 2026-11-04T10:15:00Z.
Approved: k7Qm2xR9aB has send now.
What it can do now:
...
✓ Message customers
...

A denied or expired approval exits 3 (grant_denied, grant_expired). A coding agent that hits a refusal for something its key was not granted (403, such as “can’t message customers”) gets Flow’s hint naming the keys grant to ask for, and the three tiers.

flow keys list (or flow keys) lists the live keys of the agent in the folder you run it from, never their values. The folder’s agent is the one its FLOW_KEY or FLOW_LIVE_KEY belongs to, else its package.json name; outside an agent’s folder it lists every agent’s. --agent <name> lists one agent’s, --all every agent’s on your account. --json prints { keys, agent, all } and nothing else:

Output
KID AGENT MODE KEY EXPIRES
k7Qm2xR9aB cafe live …R3ad 2026-12-04 10:00

flow keys show <kid> prints one key’s checklist (✓/✗, the same lines for every key) and what was granted to it, when and by whom. flow keys revoke <kid> ends one at once.

Flow Insights reads whole conversations, not single replies: where people gave up, asked again, asked for a person or got annoyed, grouped by topic. These are the findings the dashboard’s Insights page shows (Flow Framework 0.16.0 on):

Terminal
npx @flow-engineer/flow insights
npx @flow-engineer/flow insights show fnd_1
npx @flow-engineer/flow conversations show cnv_1

Check it worked: insights prints the open findings, ranked as the dashboard ranks them, each with its conversations in the window and the window before’s:

Output
ID STATUS CONVERSATIONS TOPIC FINDING
fnd_1 open 4 (was 9) Refunds Got annoyed when the agent got a fact wrong
fnd_2 open 3 - Unanswered
Last 7 days: 132 closed sessions from 87 people.
Low-confidence tags, left out above: fnd_3 Frustrated (5).
Read one: flow insights show <finding>

insights show <finding> prints the finding’s conversations, each with why it was flagged, the person’s words when your account’s data allows, and the command that reads it:

Output
fnd_1 open Got annoyed when the agent got a fact wrong · Refunds: 4 (was 9) conversations in the last 7 days
cnv_1 2026-10-29 09:00:00Z Got annoyed when the agent got a fact wrong
"That's not my order"
next: flow conversations show cnv_1

Read the conversations to tell whether the finding is real, fix the instructions, skill or tool behind it, add a test scenario that plays one, run flow test and deploy. Once a window has passed, run insights again: the finding’s count should drop.

  • What Insights reads: closed production conversations (quiet for 24 hours) on a live key. Conversations on a test key, in flow chat, the playground or flow test are never graded. Until 20 closed sessions, Insights shows counts only, with no rates or topics, and says Collecting. With nothing to read it says what makes data.
  • Flags: --days 7|30 (default 7), --status open|fixed|muted|all (default open, the list only), and --json for the server’s JSON.
  • Before Insights is open to your workspace, both commands print Insights is not open to this workspace yet with the server’s hint, and exit 1.
  • They need the agent’s live key: Insights is production traffic. They read with FLOW_LIVE_KEY when it is set (Read production with a live key), else a flow_live_ FLOW_KEY. With only a test key (flow_test_, what flow setup writes) both say Insights reads production conversations, so it needs a live key with the flow setup --live command, and exit 1.
  • Exit 0 when it printed, else 1, with the server’s message and hint.

A project is one agent in a workspace. These commands use your sign-in from flow login:

Terminal
npx @flow-engineer/flow projects list
npx @flow-engineer/flow projects delete cafe

Check it worked: list prints each project in your workspaces:

Output
NAME WORKSPACE ID CREATED CAN DELETE
cafe Team agt_cafe1 2026-10-01 yes
shop Team agt_shop1 2026-10-12 no (owner only)

delete asks you to type the project’s name, then prints:

Output
Deleted cafe (agt_cafe1) in Team. Its keys stopped working; the Flow team can bring it back until 2026-11-28 10:00 UTC.
  • Only the organization’s owner can delete a project. Others get exit 3 (owner_only).
  • Its keys stop working at once. The Flow team can restore the project for 30 days; ask them before restore_until (shown in --json).
  • Name or id. A name is matched exactly (case-sensitive). When two projects share a name, delete refuses and prints both ids: pass the id. --workspace <acc_id> keeps either command to one workspace.
  • Without a terminal (CI, a coding agent), delete asks nothing and needs --yes. Pass it only when the owner has said to delete that project.
  • --json prints {"projects": [...]} (id, name, workspace, created_at, owner) or {"project": {...}} with deleted_at and restore_until; errors are {"error": {"code", "message", "next"}}.
  • Exit codes: 0 done; 1 usage, not signed in, no such project, a shared name or not confirmed; 2 flow.engineer could not be reached (try again); 3 refused (owner only, or the sign-in ended).

The host runs your src/agent.ts as one Node process per environment. It calls buildAgent({ events, host }), where host carries the host’s store and, when you set FLOW_MESSAGING_KEY, Flow Messaging’s channels. Everything else comes from the environment below. A project with only src/mcp.ts runs without a chat agent (An MCP server alone).

Most agents need only FLOW_AGENT_URL, FLOW_ENVIRONMENT, FLOW_KEY and their own secrets.

VariableWhat it holds
FLOW_AGENT_URLThe environment’s public HTTPS address, also printed by deploy status. Build webhook and callback URLs from it
FLOW_ENVIRONMENTstaging or production
FLOW_KEYThe environment’s runtime key, when your .env has a FLOW_KEY (see below)
Your secretsEvery other .env value, under its own name
FLOW_AGENT_ID, FLOW_AGENT_NAMEThe agent’s id and name
AGENT_VERSIONThe deployed version; traces carry it
PORT8080. The host’s runtime listens on it; your code does not open a server
FLOW_STORE_URL, FLOW_STORE_TOKEN, FLOW_STORE_FRAMEWORKThe host’s store. Use it through host, never directly
FLOW_API_TOKENProtects /nudges and /people
FLOW_PLAYGROUND_TOKENProtects /playground
CHINWAG_TRACES_URL, CHINWAG_TRACES_KEYWhere traces go; set by Flow
FLOW_FRAMEWORK_TRACES_CONTENT, CHINWAG_TRACES_CONTENTWhether traces carry message text: 1, Flow’s default, unless you set your own 0 or 1 (below)
FLOW_WEB_CHAT_SESSION_KEYSigns web chat sessions and anonymous visitor ids; one per environment, the same on every deploy
FLOW_TRUST_PROXY1: the web chat reads the visitor’s address from X-Forwarded-For for its limits
  • Names Flow sets are refused as secret names: the ones above except FLOW_KEY and trace text, plus any name starting FLOW_STORE_, CHINWAG_TRACES_, FLOW_FRAMEWORK_TRACES_ or K_, and AGENT_DIR. deploy never sends the CLI’s own FLOW_FRAMEWORK_CLOUD_KEY.
  • Trace text is yours, per environment. deploy status shows it (trace text: on (Flow's default) or off (set by you)). To switch it off in production: echo 0 | npx @flow-engineer/flow deploy secret FLOW_FRAMEWORK_TRACES_CONTENT --env production --apply (or without --apply, then npx @flow-engineer/flow deploy promote). Only 0 and 1 are taken; deploy does not send the .env’s line, which setup writes for local runs. Deleting the secret on the dashboard goes back to Flow’s default.
  • A secret takes effect on the next deploy or promote, or now with deploy secret NAME --apply or deploy restart, which restart the live version without a build. deploy secret --unset NAME takes one away. Names are UPPER_SNAKE_CASE; values are at most 64 KB.

FLOW_KEY in the hosted process is not the developer key in your .env. When deploy sends it, Flow exchanges it for a runtime key per environment: scopes messaging, traces and model only, flow_test_… in staging and flow_live_… in production. The developer key is never stored.

  • flow deploy secret FLOW_KEY answers with the runtime key’s last 4, which differ from what flow link and your .env show. That is expected; it is the same account.
  • A key Flow refuses (revoked, or not a developer key) is not saved and does not stop the deploy. A runtime key the environment already has stays.

The agent is served at FLOW_AGENT_URL. Hosting adds these paths; every other path goes to your agent’s own routes:

PathServed whenAccess
/healthzAlwaysPublic
/webhooks/flow-messagingFLOW_MESSAGING_WEBHOOK_SECRET is set; without it, the host holds Flow Messaging’s event streamFlow Messaging’s signature
/nudges, /peopleAlwaysAuthorization: Bearer $FLOW_API_TOKEN (nudges over HTTP)
/playgroundAlwaysIts token; the Flow dashboard opens it for you
/mcp, /mcp/plugin, /.well-known/oauth-protected-resource, /agent/terminal, and your auth.routessrc/mcp.ts existsPublic: hosting adds no authentication (below)
/v1/web-chat and everything under it (the channel’s base)The agent has a webChat() channel (Framework 0.12.0 or later)Public: the chat box’s script, and starting a session from the sites in origins. Every other call needs that session
Any other pathYour createAgent({ routes }) has itYour route checks the caller
  • Exact paths only. Except for the web chat’s paths, a route matches its path exactly: no prefixes or wildcards, and the query string is ignored for matching. An unknown path returns 404.
  • Request bodies are at most 5 MB (413 above that).
  • Do not reuse the paths above for your own routes: the host answers them first.

Use model: flowModel() on Flow’s hosting. It always sends the environment’s FLOW_KEY to Flow’s model API, paid from the account’s Flow credit.

When the model comes from the environment (openAICompatible() with no key), a provider key you sent as a secret (OPENROUTER_API_KEY, OPENAI_API_KEY, ANTHROPIC_API_KEY or GEMINI_API_KEY) wins over FLOW_KEY, following the key order. Choose explicitly with flowModel() or openAICompatible({ apiKey, baseURL, model }).

The process has no disk that persists. Files it writes, such as .flow/session.json, live in memory and are lost at every restart, redeploy and scale to zero.

  • Conversations, schedules, nudges and wake-ups are kept in the host’s store automatically when buildAgent passes host: options.host to createAgent.
  • The agent’s own data goes in records, which use the same store. Data your app or team reads goes in your own database, reached through a tool.
  • Each environment has its own store. Promoting staging copies no data.
  • The OAuth bridge’s storage is host.oauthStorage, given to an mcp(agent, host) factory in src/mcp.ts.
  • Production refuses an agent that drops host or brings its own database; staging warns. An MCP server alone has no chat state, so neither applies to it.

A webChat() channel needs only your sites: list them in webChat({ origins }) or set the secret FLOW_WEB_CHAT_ORIGINS (comma-separated). For signed-in visitors, also set FLOW_WEB_CHAT_IDENTITY_KEY. Flow sets the session key and the trusted proxy.

  • Production refuses a web chat open to any site (web_chat_open); staging deploys it with a warning. Promoting such a version is refused while production has no FLOW_WEB_CHAT_ORIGINS.
  • The limits are exact here: the environment runs one process.
  • The box streams events for up to 14 minutes, then reconnects, within the 900-second request timeout. Staging that scaled to zero starts again on the first visitor’s request.

When the folder has src/mcp.ts, the host serves its export beside the agent on the same origin. Connect FLOW_AGENT_URL + /mcp through the MCP setup.

Without auth, a deploy warns mcp_open. Once the hosting update now rolling out is live, it names only the tools that need sign-in (signIn "required" or "optional"; an older framework’s tools all count), so an agent whose tools are all signIn: "none" gets no warning. It never names Flow’s built-in feedback tool: it says what it is, and that mcpServer({ feedback: false }) leaves it out.

A project that only serves MCP, with src/mcp.ts and no src/agent.ts (no buildAgent, model key or channels), deploys the same way. The host calls a factory as mcp(undefined, host): agent is undefined and host.oauthStorage is the OAuth bridge’s storage in the host’s store.

src/mcp.ts
import { mcpServer } from "@flow-engineer/flow/mcp";
import { oauthBridge } from "@flow-engineer/flow/oauth";
import { tools } from "./tools.ts";
export { tools }; // flow check reads them here when there is no src/agent.ts
export async function mcp(_agent: undefined, host) {
const bridge = await oauthBridge({ ...bridgeOptions, storage: host.oauthStorage });
return mcpServer({ name: "acme", version: "1.0.0", tools, auth: bridge.auth });
}
  • It serves /healthz, its MCP paths and the bridge’s routes, and its CLI’s install (src/cli.ts) if it has one. Every other path answers 404: there is no playground, /nudges, /people, agent route or Flow Messaging. A FLOW_MESSAGING_KEY secret is logged as unused.
  • Production takes it. Its state is the bridge’s records in the host’s store, so the deploy check skips the chat rules. It still checks the Flow Framework version and the MCP sign-in warnings (mcp_open, mcp_public_tools), and refuses a src/mcp.ts that exports no mcp (invalid_mcp).
  • It needs @flow-engineer/flow 0.16.1 or later for flow deploy and flow check to accept the folder, and the hosting update now rolling out.
  • One instance at most per environment, with 1 CPU and 1 GiB of memory. Files written in memory count against that.
  • Requests time out after 900 seconds.
  • Production keeps one instance warm. Staging scales to zero when idle unless the agent has FLOW_MESSAGING_KEY; while it is scaled to zero, nothing in it runs until the next request starts it.

A business event, a payment provider’s webhook or a third-party login redirect (a broker’s OAuth callback, for example) is one of the agent’s routes. The host sends every path it does not serve itself to agent.fetch(request), which answers from routes. Register the full URL, FLOW_AGENT_URL plus the path, with the other service.

src/agent.ts — inside buildAgent
const agent = createAgent({
// ...the starter's options, host: options.host included
routes: {
"/webhooks/orders": async (request) => {
if (request.headers.get("authorization") !== `Bearer ${process.env.ORDERS_WEBHOOK_TOKEN}`) {
return new Response(null, { status: 401 });
}
const { phone, orderId } = (await request.json()) as { phone: string; orderId: string };
const sent = await agent.nudge({
to: { channel: "whatsapp", address: phone },
prompt: `Order ${orderId} has filled.`,
idempotencyKey: `order-filled-${orderId}`,
});
return Response.json(sent);
},
"/oauth/broker/callback": async (request) => {
const url = new URL(request.url);
// Match url.searchParams.get("state") to the sign-in you started, then exchange "code".
return new Response("Signed in. You can go back to the chat.");
},
},
});
return agent;
  • The route checks who called: a shared secret, the provider’s signature, or the OAuth state you issued. Hosting passes every request through unauthenticated.
  • ORDERS_WEBHOOK_TOKEN is your own secret: put it in .env and deploy.
  • This is the supported way to add a webhook or callback. mcpServer({ auth: { routes } }) is only for the OAuth bridge’s own endpoints.
  • Locally, npm start serves the same routes on the playground’s address (Framework 0.9.0 or later, localhost only), so you can call them before you deploy.
  • To message the person outside WhatsApp’s 24-hour window, see a business event, later.
The buildAgent contract and what deploy sends

For a custom entry point, export buildAgent(options) from src/agent.ts and pass host: options.host to createAgent as one option. Do not spread it into the agent options. Keep the agent’s own dir.

deploy needs package.json, package-lock.json and src/agent.ts (or only src/mcp.ts for an MCP server alone). It uploads the project without node_modules, Git data, .flow or environment files (at most 50 MB), then sends eligible environment values separately as secrets. Use --no-secrets to skip sending them.

The deployment reference covers secret updates, flags, JSON output and exit codes.