For coding agents
Build, connect, test and deploy a Flow project with CLI exit codes and next steps.
The published guide is at https://flow.engineer/docs/agents: that is the link to give.
You are building or changing a Flow project for a person (the owner): tools and skills exposed
through an MCP server (Claude, ChatGPT, Cursor, Codex), plugins, a CLI or a messaging agent on
WhatsApp, iMessage, Telegram, SMS or their own app. Start with the interfaces the owner needs.
Every command here runs without a terminal and never reads stdin, except chat, which reads the
messages you pipe to it, and projects delete, which asks for the project’s name in a terminal
(without one it needs --yes).
Always name the package: npx @flow-engineer/flow .... npx flow on its own fetches another npm
package.
Starting from the owner’s product
Section titled “Starting from the owner’s product”When the owner has a product (a codebase), the agent is built from it:
- Read the product’s code (what it does, who uses it, its API, how users sign in). Not in its folder? Ask the owner for the project’s path or a few lines about it.
- Propose what goes first and wait for a yes: an MCP server and a CLI for a business or developer product (SaaS, B2B, a dev tool); a WhatsApp and iMessage agent, plus the MCP server and CLI, for a consumer app. Ask what to call the agent in the same message.
npx @flow-engineer/flow@latest new <name>-agent --display-name "<name>"beside the product’s code (never inside its package), thennpm installthere.- Read
.claude/skills/flow-framework/SKILL.mdfor setup and routing. Then load only the relevant skills:flow-framework-build-agentfor messaging runtime and operations,flow-framework-mcpfor MCP, CLI and plugins, and the context, tools, auth or tests skills when those tasks apply. - Replace the café with the product: context, tools,
agent/AGENTS.md, sign-in, then check it (npx @flow-engineer/flow check,npm run mcp,npm run cli -- --help, and the phone below).
MCP and CLI first
Section titled “MCP and CLI first”After creating the project and running npm install, start the server with npm run mcp.
In another terminal, use npm run cli -- --help and npm run cli -- menu --json. The starter
prints the MCP URL and a command to connect Claude Code. These tool calls need no model key or
Flow account. Replace the café tools and configure account sign-in when needed.
For remote hosts such as Claude or ChatGPT, expose the server over HTTPS and connect its URL in the host. Generating a plugin does not publish it to a directory; follow the host’s installation or review flow. See MCP servers, plugins and CLIs.
In Framework 0.8.0, tool({ ..., mcp: { deferred: true } }) puts a less-used tool behind MCP’s
search_tools and execute_tool; unmarked tools stay upfront. Chat agents and CLI commands keep
their normal access. mcpServer({ deferred: [...] }) replaces the individual flags for that server,
and deferred: [] puts everything upfront. See deferred tools.
Messaging setup
Section titled “Messaging setup”For phone testing, link sets up the keys and an outbound event stream, with no tunnel or public
URL needed on the developer’s laptop. For the chat agent, it can also supply a trial model key.
npx @flow-engineer/flow init --display-name "FlatX agent" # the starter in this (empty) folder; ask the owner what to call it firstnpm installnpx @flow-engineer/flow link --channel telegram --json # or whatsapp, imessage: signs in and sets up if needed (the keys, Flow's trial model key among them), then prints the phone linknpm start # after link: keep it running; the playground link it prints, and the phone link reaches itnpx @flow-engineer/flow status --json # any time: what the folder has and lacksprintf 'hi\nA latte please\n' | npx @flow-engineer/flow chat # your own check: talk to it locally, one message per lineBefore init (or flow new <folder>), ask the owner what people should call their agent, such as
“FlatX agent”, and pass it as --display-name. It is kept as displayName in package.json, and
the message their phone sends to reach the agent is Join FlatX agent (ref <code>). The name takes the
starter’s (Pip) place in the agent’s prompt, agent/AGENTS.md, and the playground and MCP server
read it from package.json. To rename an agent made already, link --display-name "<name>" (or
setup --display-name) changes both; restart npm start after it. Without the flag, init asks in
a terminal and otherwise uses the folder’s name in words.
npm start listens on PORT, else 8788: with 8788 taken and no PORT set it takes the next free
port and says so, and the playground link it prints has the port it uses. With PORT set to a port
that is taken it prints one line, Port 8788 is in use. Run: PORT=8798 npm start, and exits 1.
npm run mcp does the same with MCP_PORT (else 8790).
For a chat box on the owner’s own website (Framework 0.12.0 or later), add
webChat({ origins: ["https://www.example.com"] }) to channels and remove the starter’s
customUI("web", provider) (both are named web); the site adds one tag,
<script async src="https://<agent address>/v1/web-chat/widget.js"></script>. Always list the
sites. For a signed-in visitor, put the number the backend verified in the identity token’s phone
(E.164), never one typed in the chat: each turn is then shown what they said lately on WhatsApp,
SMS or iMessage from that number, and back, while replies stay on the channel they wrote on.
Signed-in visitors, page context, limits and the site’s CSP are on Web chat.
How the owner tests it
Section titled “How the owner tests it”Match the way they test to the interface they are building. For MCP, connect the server in their chosen host and try a real product request. For a CLI, show its help and a command that reads data. For a plugin, install the generated package in the host and exercise its tools and skills.
For a messaging agent, recommend these, in this order:
- The playground in the browser:
npm start, then open the link it prints. The test people (testPeople; Maria, Sam and Priya in the starter) and the channels are side by side, and Debug shows each tool call and how fast each message was answered. - Their phone: the link from
link(say_to_personinlink --json), whilenpm startruns. It is the channel’s own link (url):https://t.me/<bot>?start=<code>(press Start),https://wa.me/<number>?text=...or, for iMessage,sms:<number>?&body=...(an iPhone or a Mac), each withJoin <displayName> (ref <code>)filled in (join_message).pageis the same on flow.engineer, for a computer without that app.
flow chat and npm test -- --synthetic (the test cloud) are for you, to check your own work
before you hand it back; run them yourself, and do not offer them to the owner as ways to test.
From Framework 0.14.8 the test run exits 0 when 98% or more of its counted runs pass, so read its
failed runs even then, or pass --min-pass 100 (pass rate).
- The model key: a provider key is optional.
setup(whichlinkruns; alone it needsloginfirst) writesFLOW_KEY, the agent’s one Flow key, into.env(printed only as its prefix and last 4,FLOW_KEY=flow_test_…abcd added; never read or print its value): with no provider key the agent calls models through Flow with it, on the account’s $2 of Flow trial credit (printed asModel: Flow trial credit ($1.87 left)). The owner may add their own: one ofOPENROUTER_API_KEY,OPENAI_API_KEY,ANTHROPIC_API_KEYorGEMINI_API_KEYin.envwins overFLOW_KEYfor the model (to switch back, empty that line), and a key they set is never written over. When the credit is used up, a model call fails with codetrial_credit_used(trial_credit_unavailablewhen the account gets none) and the next step: add your own key. With no key,chatandnpm startstop with one sentence andNext: run npx @flow-engineer/flow setup to get Flow's free trial credit, or add your own key.(chat --json:{"error":{"code":"no_model_key","message":...,"next":"npx @flow-engineer/flow setup"}}), andinit,setup,linkandstatussay so too, with the way to get one first innext. Each folder’ssetupmakes its ownFLOW_KEY(the credit is the account’s);setup --rotate keyreplaces a leaked one (the old one works 24 h more;--nowends it at once). - From 0.8.1, with a test traces key (
otk_test_, the Staging dashboard),setupalso writesFLOW_FRAMEWORK_TRACES_CONTENT=1below it, under a comment saying it sends message text there and that 0 sends metadata only, so playground and phone chats can be read and graded. It never does for a live key, or when.envalready has the line (any value), and only in the run that writes the key: a later run that keeps the key does not put back a line the owner deleted (to keep content off across--rotate traces, set it to 0). setup --workspace <name|acc_id>(andlink --workspace) puts the agent, its cloud and traces keys in that observe workspace of yours and keeps itsacc_id in.envasFLOW_FRAMEWORK_WORKSPACE, which later runs send; another workspace makes those two keys again (403not_a_member, 409workspace_ambiguous: pass the id).statusreportsFLOW_KEYand the model key (never their values;flow_key.setin--json; withFLOW_KEYthe model’s key, the credit left from Flow,model_key.flow_credit; a key Flow does not know getssetup --rotate key), the workspace (FLOW_FRAMEWORK_WORKSPACE, elsepersonal), the sign-in, each channel’s sender in.envand, signed in, which channels are open now with their free-trial counts (the account’s, across all its agents; each channel’saccount_trialin--json). It writes nothing.chatbuilds the agent fromsrc/agent.tsand writes as its first test person. Each reply is printed (agent: ...) once the turn is over;--jsongives one JSON line per thing it sends.linkdoes the rest when the folder is not set up (noFLOW_FRAMEWORK_JOIN_CODEorFLOW_MESSAGING_KEYin.env, or no sender for the WhatsApp or iMessage link asked for): it runslogin(opens the owner’s browser itself and prints the link and code at once), thensetup(writes the keys and each open channel’s sender into.env, and the chosen channel’s page asFLOW_FRAMEWORK_TRY_URL), then prints the link and that channel’s free-trial line only.--channel telegram|whatsapp|imessagepicks the channel (Telegram by default). Run it in the agent’s folder:setupandlinkrefuse a folder with nopackage.json(not_an_agent_folder).- A channel that is not open yet is said once (“iMessage isn’t open yet; here are your Telegram
and WhatsApp links”), with the open channels’ links, and exits 0:
link --jsonthen hasnot_open(the channel asked for),links(each open channel’s) andurl/channelfor the first of them. Only when no channel is open does it exit 3 (channel_not_open). - Texting needs only Flow Messaging’s part of setup (and a model key). When setup’s answer has it
(the join code, the Messaging key and senders) but observe’s part failed (traces and the test
cloud:
observe_failed, or observe’s own limit),linkprints one warning line on stderr,Traces and tests won't work until .env has FLOW_FRAMEWORK_CLOUD_KEY and FLOW_FRAMEWORK_TRACES_KEY: <the server's reason>. Then: <its next>, then the link, the channel’s trial line and itsNext:as usual, and exits 0;link --jsonhas the line inwarnings, on setup’s line and on its own. Give it to the owner;setupmakes the missing keys once it is fixed. Run again,linkfinds Messaging’s part in.envand does not run setup.setupon its own still exits 3 for it. - On iMessage the agent can reply after the owner writes first, any time later. On WhatsApp it
can reply within 24 hours of the owner’s last message. On both it cannot message them first.
link --jsonsays so (reply_only: true, and insay_to_person): tell the owner. - The starter’s
npm startis on Telegram, plus WhatsApp and iMessage when.envhas their senders (FLOW_MESSAGING_WHATSAPP_SENDER,FLOW_MESSAGING_IMESSAGE_SENDER), and reads.envonce.npm startmarks itself running in.flow/running.json, solinkends with the step that fits:Next: npm start, then send the link abovewhen it is not running; when it is,Next: restart npm start (it runs at <url> and lacks iMessage), then send the link aboveiflinkjust wrote.envor renamed it, elseNext: send the link above (npm start is running at <url>)(link --json:running,falseor{"url"}, andnext).setupalone says “If npm start is running, restart it to add iMessage.” - Give the owner
say_to_personfromlink --json, and the sign-in link and code from thependingline while you wait. npm startconnects to Flow Messaging by an outbound event stream. Messages sent while it was stopped arrive when it starts again (for as long as Flow keeps events, 30 days by default).
Exit codes (login, setup, link)
Section titled “Exit codes (login, setup, link)”status exits 0 whatever it finds (1 for a wrong flag): read its fields.
| Code | Meaning | What to do |
|---|---|---|
| 0 | Done | Go on |
| 1 | Usage, not signed in, not an agent’s folder, or a login URL that is not https | Fix the command, or run next |
| 2 | flow.engineer could not be reached, or did not answer in time (timeout) | Try again |
| 3 | Refused: denied, expired, a product failed (for link, only when Messaging’s part did: observe’s alone is exit 0 with a warning), no channel open yet (one closed while others are open is exit 0 with their links), a server with no CLI sign-in (server_unsupported), or a free-trial limit (sandbox_allowance_used, the iMessage people cap; HTTP 403) | Run next; for a trial limit next is a sentence: give it to the owner as it is |
| 4 | Sign-in still waiting for the owner’s approval, or flow.engineer asked to wait (slow_down, HTTP 429) | Approval: ask the owner to approve, then run next (the same code is kept). slow_down: wait retry_after seconds (printed as Wait <n> s), then run next |
login and link wait at most --timeout seconds (120 by default) before exit 4, and no single
request waits longer than 30 s. login --wait (or link again) goes on with the same code: no new
code and no new browser tab. flow.engineer decides when a code has expired (an approved one can be
collected a while after its expires_in); a waiting code that has expired is replaced by one new
sign-in, and so is a code from another login URL.
The next lines
Section titled “The next lines”Every error is {"error":{"code","message","next"}} on stdout with --json, and
<message> Next: <next> on stderr. A waiting sign-in is {"status":"pending",...,"next":...}.
next is the exact command or URL to run; when it came from flow.engineer it is the server’s.
A run gives one next: setup --json’s is a list in order (a model key first when there is
none, then npm start for the playground, then link for the phone); inside link setup’s line
has none, and link’s next is one string (a model key first when there is none, then starting or
restarting npm start); an error’s next replaces it.
Run it, or give it to the owner when it needs them (a browser approval, a page to open). A
slow_down error also has retry_after (seconds, or null when the server gave none). A
trial limit’s next is a sentence, not a command: it is printed on its own line as it came; pass
it on to the owner unchanged.
flow.engineer may add warnings (up to 20 lines) to any setup answer. Each is printed on its own
line on stderr; with --json setup’s line has warnings (an empty list when there are none), and
an error line has it when there were any. Pass them on to the owner.
setup also shows the free trial. The counts are the account’s, shared by all its agents, so a
new agent can show people already (your account's free trial, across all its agents: 3 of 25 people, 2 waiting for a place; account_trial.<channel> {limit, used, held} in --json). A
person past the limit is held: their messages are kept and reach the agent once a place frees up. On
iMessage it also shows the people it may reach (account_trial.imessage.contacts {limit, used}).
trial is the older name of account_trial, kept for a release: read account_trial.
chat exits 1 for anything that stops it before the first message: no_model_key, no_agent
(no src/agent.ts), not_built, no_test_people, no_channel; with --json as
{"error":{"code","message","next"}} on stdout.
- Never ask the owner to paste a key into the chat, and never print, read aloud or commit
.env.setupwrites the keys itself and prints only their last 4 characters. - The model key: Flow’s trial key from
setup, or the owner’s own, typed into.envthemselves. setup --rotate cloud,traces,messaging,model(orall) makes those keys again and replaces their lines in.env.setupkeeps every value already in.env(--replacetakes the new ones), but a name there with no value (cp .env.example .env) counts as missing and is filled on its own line. Keys flow.engineer made are written even when the rest of setup fails, since they are shown only once..envis set to mode 0600.- The sign-in token goes only to the server that issued it, over https (http only to localhost):
with
FLOW_FRAMEWORK_LOGIN_URLnaming another server, commands stop withsigned_in_elsewhere. - CI never signs in:
npx @flow-engineer/flow setup --cisays how to give it the test cloud key.
Projects
Section titled “Projects”npx @flow-engineer/flow projects list --json # every project in your workspaces: id, name, workspace, created_at, ownernpx @flow-engineer/flow projects delete <name|id> --yes --json # only when the owner has said to delete that projectBoth use the sign-in from flow login. list takes --workspace <acc_id> to keep to one
workspace; owner says whether this account may delete the project (only the organization’s
owner can). delete matches a name exactly (case-sensitive) and refuses a name two projects share
(ambiguous_name, with each one’s id and workspace in matches: pass the id). A deleted project’s
keys stop working at once; the Flow team can bring it back until restore_until (30 days). In a
terminal it asks for the name to be typed; without one it needs --yes. Never pass --yes
without the owner’s word for that project.
Exit codes: 0 done; 1 usage, not signed in (not_signed_in: run next), no such project
(not_found), ambiguous_name, or not confirmed (confirm_needed, not_confirmed); 2
flow.engineer could not be reached or could not reach its projects (try again); 3 refused:
owner_only, or the sign-in ended (signed_out: run next).
Messages the business starts
Section titled “Messages the business starts”A nudge, a scheduled message or any turn the person did not start goes with link previews off
(preview: false on the message; Flow Messaging’s link_preview: false): on WhatsApp a
preview card’s title becomes the phone’s notification instead of the message’s text. For a call to
action use link (agent.nudge({ to, text, link: { label, url } }), or the agent’s send with
link), not a URL in the text.
When the business’s backend should tell people what happened (an order shipped, an item is back),
mount flowEvents instead of writing a route: one signed POST /v1/events, each event sent by its
delivery level (critical at once, standard after quiet hours, optional merged or dropped), with the
agent’s own wording checked against the template’s values. The envelope, signing, answers and a
mounting sample are in docs/events.md in the package
(https://github.com/flow-engineer/flow/blob/main/docs/events.md).
The agent’s own records
Section titled “The agent’s own records”What the agent itself must remember across days and people, and nothing else reads (a buyer’s
question waiting for the seller’s answer), goes in records: collections declared on
createAgent({ records: { questions: { people: ["buyer", "seller"], fields: ["listing", "status"] } } }),
context.records in a tool and agent.records elsewhere (put, get, query by equality on
declared fields, delete, forgetPerson, export). Put each field that holds a person
(context.person) in people, so forgetting them deletes the record. Keep records small (64 KB
each, 10,000 an agent): if anything other than the agent reads the data, it belongs in the owner’s
database, through a tool. npx @flow-engineer/flow records export prints them as JSON Lines.
Needs 0.6.0 or later. See the agent’s own records.
Wake-ups
Section titled “Wake-ups”When the agent must look again later and no one will write (no answer from the seller in 24
hours), use a wake-up, never a timer or setTimeout. It is a row in the store that gives the agent
a turn in that conversation at its time, with its note, and sends nothing by itself. The model has
wake_me_up and cancel_wake_up built in (createAgent({ wakeUps: false }) removes them).
From a tool, context.wakeUps?.add({ inMs, note, key }) sets one for this person, and
context.wakeUps?.cancel(key) cancels the active ones with that key in every conversation. Outside
a turn, use agent.wakeUp({ to, ... }), agent.cancelWakeUp(key) and agent.listWakeUps(to).
A wake-up keeps quiet hours. After STOP or during a pause it runs, but Flow refuses any send to the person in that turn. Past WhatsApp’s window, only a template reaches them. Limits: 10 per person, 10,000 per agent, 90 days ahead.
agent.cancelNudge(key) cancels a nudge sent with at, by its idempotency key. Needs 0.8.0 or
later. See wake-ups.
Shared test numbers and privacy
Section titled “Shared test numbers and privacy”The free trial runs on Flow’s shared Telegram bot, WhatsApp number and iMessage line. Sending an
agent’s join message (Join <name> (ref <code>), what the link fills in) binds that phone to that
agent’s app on the shared sender, so the person’s messages go only to that app; sending another
app’s join message moves them there. Trial chats are
test data, kept for the account’s retention (30 days by default). Do not use the trial for real
customers’ data: connect your own number for that.
Stability
Section titled “Stability”Flow Framework is 0.x. Existing commands, --json fields and exit codes remain compatible;
a breaking change requires a minor version bump and a migration note. Pin the version in
package.json for production.
Host it
Section titled “Host it”npx @flow-engineer/flow deploy puts the agent on Flow’s hosting, so it answers while the laptop
sleeps. It needs package.json, package-lock.json and src/agent.ts, whose buildAgent passes
options.host to createAgent as host: options.host (never spread into its options), and the agent’s CI key in FLOW_FRAMEWORK_CLOUD_KEY (the
environment, else .env; flow setup writes it).
One command deploys, the first time included:
npx @flow-engineer/flow deployIt sends the .env’s values as the agent’s secrets, then builds and follows it to live. The rest:
npx @flow-engineer/flow deploy [--env staging|production] [--version <v>] [--no-secrets] [--json]npx @flow-engineer/flow deploy promote [--no-secrets] [--json]npx @flow-engineer/flow deploy status [--env staging|production] [--json]npx @flow-engineer/flow deploy restart [--env staging|production] [--json]npx @flow-engineer/flow logs [--env staging|production] [--since 1h] [--until <t>] [--severity WARNING] [--grep <text>] [--limit n] [--follow] [--json]npx @flow-engineer/flow conversations [--env staging|production] [--grade wrong|unhelpful|good] [--since 24h] [--limit n] [--contact <label>] [--json]npx @flow-engineer/flow conversations show <id> [--json]npx @flow-engineer/flow issues [--status open|resolved|ignored|all] [--json]npx @flow-engineer/flow issues test-case <id> [--json]npx @flow-engineer/flow insights [--days 7|30] [--status open|fixed|muted|all] [--json]npx @flow-engineer/flow insights show <finding> [--days 7|30] [--json]npx @flow-engineer/flow deploy secret NAME [--env staging|production] [--apply] < value.txtnpx @flow-engineer/flow deploy secret --unset NAME [--env staging|production] [--json]npx @flow-engineer/flow deploy secrets --from-env [<file>] [--env staging|production] [--json]deployfirst sends each non-empty value in the agent’s.envas a secret for that environment, before the service starts, so the first live version has its keys: never the CI key, the cloud’s address, whatsetupandlinkwrite for the CLI alone (FLOW_FRAMEWORK_JOIN_CODE,FLOW_FRAMEWORK_TRY_URL,FLOW_FRAMEWORK_WORKSPACE) or a name Flow sets itself (FLOW_FRAMEWORK_TRACES_*,FLOW_STORE_*,PORT, …). TheFLOW_MESSAGING_<CHANNEL>_SENDERids are sent: the hosted agent starts conversations from them. A live Flow key (fk_live_,flow_live_,otk_live_) is never sent to staging: it is skipped with why (naming only its prefix), staging keeps the value it has, and Flow’s hosting refuses it too. A test key sent to production goes, with a warning.FLOW_KEYgoes to both: the host exchanges it for the environment’s own runtime key. An earlier deploy may have sent those three CLI names; they are harmless, andnpx @flow-engineer/flow deploy secret --unset FLOW_FRAMEWORK_JOIN_CODE --env stagingremoves one. Later deploys send only the new or changed ones (.flow/secrets.jsonkeeps a hash per name, never a value). It prints the names it sent, never values;--no-secretssends none. A secret the host refuses stops the deploy before it starts, naming it.deploypacks the folder (withoutnode_modules,.git,.env*and.flow, at most 50 MB), uploads it, and follows the build until it is live, refused or failed. The default is staging; production refuses an agent that ignoreshost, brings its own database or has a web chat open to any site (web_chat_open: list the sites inwebChat({ origins })or the secretFLOW_WEB_CHAT_ORIGINS), and the build refuses an agent on a newer framework than Flow hosts yet.--jsonprints{ ok, deploy: { id, environment, status, version, findings, error }, url, next }on stdout (urlis null when the cloud does not say it yet; the human output then says “URL: not available on this Flow yet”); progress goes to stderr. The deploy’s id is kept in.flow/deploys.json. The version is the agent’s version, the label its traces andflow testsend:FLOW_AGENT_VERSION, else the CI’s commit (GITHUB_SHA, …), else the folder’s full git commit,<sha>+dirty(with a warning) when the folder has uncommitted changes, since what is packed is the folder as it is, elsepackage.json’sversion;--versionnames it yourself. Hosting gives it to the agent asAGENT_VERSION, so its traces carry it. (Before Framework 0.10.3: the short commit.)promotemoves staging’s live version to production, with no new build. Production keeps its own secrets: promote sends the.env’s new or changed values there first (--no-secretssends none).statusshows, per environment (both unless--env), the deploy serving it (status,version,id,updated_at), its URL and a newer one still building, and whether its traces carry message text:trace text: on (Flow's default)ortrace text: off (set by you)(deploy.trace_text,{ on, source: "flow" | "owner" }; Framework 0.14.2 on).--jsonprints{ ok, agent_framework, hosting_framework, environments: { staging: { deploy, pending, url, from } | null, production: ... }, next };fromislist(the cloud’s list) orthis_folder(the last deploy from.flow/deploys.json, on a Flow whose cloud does not list deploys to a CI key yet). First, the Flow Framework this folder’spackage.jsonpins (agent_frameworkin--json, 0.16.0 on); then, when the cloud says them (Framework 0.16.0 on), the newest line Flow’s hosting runs, which is hosting’s and not the agent’s (hosting_framework), each deploy’sbuilt with:line (the framework its build used,deploy.framework), andtraces:per environment, whether Flow’s dashboard takes its traces:ok(with when it last took one),quiet(none yet), orREFUSED (<status> <code>)with the reason and hint (environments.<env>.traces).- Trace text is on by default in hosting. To switch it off in production, set the secret, then
promote:
echo 0 | npx @flow-engineer/flow deploy secret FLOW_FRAMEWORK_TRACES_CONTENT --env production, thennpx @flow-engineer/flow deploy promote(echo 1switches it back on). Deleting that secret on the dashboard (the agent’s secrets) goes back to Flow’s default at the next deploy. logsprints the hosted agent’s log lines from--since(30s, 15m, 1h, 2d, up to 30d, or an RFC 3339 time; default 1h) to--until(the same forms; not with--follow), and with--followkeeps printing new ones. A duration is that long before now for both flags (--since 3d --until 2dis the day before yesterday), a time is taken as given, and--untilmust be after--since; MCP’sread_logstakes the same rule.--severity DEFAULT|INFO|WARNING|ERRORkeeps that severity and above, and aDEFAULTline that is plainly an uncaught error (Error:,DOMException, a stack: how runtimes before 0.16 wrote Flow’s own errors) counts asERROR;--grep <text>keeps lines containing it anywhere in the line as printed, a JSON line’s every field and value (case-insensitive, at most 200 characters). The cloud searches the fields’ values; a--grepwith a quote or a brace ("step":"x", JSON as printed) is matched by the CLI over every line of the window instead. The cloud filters; the CLI filters again, so an older cloud’s answer is right too. A stack trace Cloud Run split into an entry per line prints as one line: a line starting with whitespace (at ...) within 2 ms of the line before it (Cloud Run stamps each line microseconds apart), from the same instance, joins it, and the joined line keeps the higher severity. When the lines start later than--sinceasked, stderr saysLogs start at <time>: this Flow keeps <N> back. Every page up to--until(else now) is read before printing, 500 lines a request (the cloud’s limit); past--limitlines (default 10000, at most 100000) the newest are kept and stderr says where they start and the--since/--untilfor the older ones.--jsonis one{ time, severity, text }per line. On a Flow whose cloud has no logs route yet it says so:error.codenot_available, exit 1. Flow’s own lines carry their real severity, and a failed tool call is oneWARNINGorERRORline (Framework log lines), sonpx @flow-engineer/flow logs --since 1h --json | jq -c 'select(.severity == "ERROR" or .severity == "WARNING")'lists only the problems.conversationslists the hosted agent’s conversations, newest first (id, last message, channel, contact, messages, and how many replies were graded wrong and unhelpful), over as many pages as--limitneeds (default 20, at most 1000).--sinceis 30m, 24h, 7d or an RFC 3339 time.--contact "Contact 4f1c"(or4f1c) keeps one contact’s conversations, read over as many pages as it takes.--envand--environmentare one flag.conversations show <id>prints one, one line per message, and under each graded agent reply its label, rule, reason and quoted words. When the cloud sends them, each agent reply also shows its tool calls (tool <name> <ms> ms ok, orFAILED: <code>: <message>, then itsinput:andoutput:when Flow keeps them, masked and cut at 2 KB,(cut by Flow)) and its delivery (queued,sent,delivered,read, orFAILED <channel code> (<type>)); a message whose text was not sent to Flow says why (metadata-only traces,FLOW_FRAMEWORK_TRACES_CONTENToff, or a photo, video or voice note with no caption).--jsonpassestoolsanddeliverythrough.issueslists the problems the grades add up to, each with a suggested fix (--status openby default), then the team’s feedback of the last 30 days (Framework 0.14.4 on):check out of date <scenario> (<channel>), check <n>: <check>(update that scenario check) andgrade marked wrong conversation <id>, reply <id>, rules <ids>(do not change the agent for it), each with itsnote:andtodo:;--jsonstays the issues only. They read with the agent’sFLOW_KEY(.env, or--dotenv; never printed) at--cloud, elseFLOW_FRAMEWORK_CLOUD, else flow.engineer. Without--environment, a live key reads production and a test key staging. Contacts show only as a label (Contact 4f1c), never an address. Text Flow keeps back prints as(text withheld)or(deleted by retention), and messages the agent’s own code sent show asyour code.--jsonis the server’s JSON (pages merged into one). Exit codes: 0 printed, 1 no key, refused or not reachable, 2 a wrong flag (Framework 0.16.0 on; 1 before), with the server’s message and hint, and on a refused key which key the command reads, which one it found (prefix and last 4) and where. To fix bad replies:conversations --grade wrong, thenconversations show <id>; fix the instructions, skill or tool the rule points at, add a scenario that plays that conversation, runflow test, and deploy. The list’s KIND istestforflow chat, the playground andflow test(never graded, never in Insights;showsays so), elseproduction.issues test-case <id>prints the test case Flow proposes for an issue (rule, the reply it comes from,includes,must_not,expect,rubrics), to write a scenario from; it writes no file (Framework 0.16.0 on).insightslists Flow Insights’ findings (where people gave up, asked again or got annoyed in closed production conversations on a live key, by topic), ranked as the dashboard ranks them: id, status, conversations with the window before’s (4 (was 9)), topic and what happened; low-confidence tags are named under the table.--days 7|30,--status open|fixed|muted|all(default open).collecting(under 20 closed sessions) shows counts only.insights show <finding>lists its conversations, each with why it was flagged, the person’s words when allowed andflow conversations show <id>. Before Insights is open to the workspace both printInsights is not open to this workspace yetand exit 1 (Framework 0.16.0 on). They read withFLOW_LIVE_KEYwhen it is set (flow setup --live), else aflow_live_FLOW_KEY: with only a test key (flow_test_) both say so, namingflow setup --live, and exit 1. The loop:insights,insights show,conversations show, fix, a scenario,flow test, deploy, andinsightsagain after the window.secret NAME(one value from stdin) andsecrets --from-env(every value in a file, now) are for changing a secret by hand;deployalready sends the.env. In a terminalsecretasks for the value without echoing it (one line); a value of several lines is piped in.--jsonprints{ ok, environment, saved, skipped, refused, next }. Before the agent’s first deploy the host has no agent to keep them for: they are refused, exit 1, andnextisdeploy. A secret takes effect on the next deploy or promote, unless--applyrestarts the live version with it now.restart(andsecret --apply) restarts an environment’s live version with its secrets as they are now, no new build, and follows it to live likepromote; with nothing live it says so and exits 0.secret --unset NAMEtakes a secret away (it applies at the next deploy or restart). A Flow whose cloud cannot do either yet says so (not_available, exit 1).- Exit codes: 0 live or saved, 1 refused, failed or not reachable, 2 a usage problem (no CI key, a
missing file, a bad name). On an error,
--jsonprints{ ok: false, error: { code, message }, next }.
For coding agents: JSON, waits and keys
Section titled “For coding agents: JSON, waits and keys”--jsonis one JSON document on stdout, a failure’s too:{ "ok": false, "error": { "code", "message", "hint"? }, "next" }, exit 1 (2 for a wrong flag on the cloud reads), from every command that takes--json, a wrong flag included (Framework 0.16.0 on). The account commands’ errors (login,setup,link,keys,projects, …) carrynextinsideerrortoo. Progress and notes go to stderr, so read stdout alone; do not merge it with2>&1. Two commands are JSON Lines instead:logs --json(one line per log line) andlogin,linkandsetup(one complete document a line; the last line is the answer).- Rate limits. Flow’s cloud reads 60 times a minute per key. A
429is waited out for itsRetry-After(at most 30 s, said on stderr) and sent again, up to 3 times; a GET is tried once more after a network error or a 5xx.loginkeeps polling throughslow_down. - Which key.
conversations,issuesandtemplatesread withFLOW_KEY:flow setupmakes aflow_test_key, which reads staging. Production reads (conversations --env production,insights, andconversations showof a production conversation) useFLOW_LIVE_KEYwhen it is set, elseFLOW_KEY: the workspace’s owner makes it withflow setup --live, for 30 days and de-identified text by default (the key table and its limits). It messages no customer and deploys nothing live: ask for more withflow keys grant <name>, by tier. Fine by default (de-identified reads, Insights, tests, traces, the model): granted at once. Only when the user asked: raw message text (keys grant content). Only with the user’s explicit yes: messaging customers and deploying live (keys grant send|deploy); the owner confirms once in the browser (granting more). Every read prints aText:line and--jsoncarriestext_mode.test,show,deploy,logsandcli publish|rollback|statususe the CI keyFLOW_FRAMEWORK_CLOUD_KEY(oci_...), from the agent’s Developers page on the dashboard orflow setup. A refused key’s error names the key wanted, the one found (prefix and last 4, never the value) and whether it came from the environment or.env. - Cells. The US cloud is
https://flow.engineer/observe(the default;/dashboardis the same); the India cell’s ishttps://flow.engineer/dashboard/in: setFLOW_FRAMEWORK_CLOUD(or--cloud) to it for an agent there. A network failure names the address tried and the error (ENOTFOUND,ECONNREFUSED, …).
Run each answer’s next. Promote, or deploy to production, only when the owner says so.
What the hosted process gets (its environment, the runtime FLOW_KEY, the routes it serves, the
model, storage, limits) and how to add a webhook or OAuth callback are on
Deployment. On Flow’s hosting /mcp is public: without
mcpServer({ auth }), every tool runs for anyone who reaches it, signIn: "required" ones included.
Mark a tool that is public on purpose signIn: "none", as the starter’s are.
Flow’s MCP server for your coding agent
Section titled “Flow’s MCP server for your coding agent”flow mcp serve gives a coding agent the same reads as typed MCP tools, so it calls a tool
instead of running a command and parsing --json (Framework 0.16.0 on). Add it from the agent’s folder:
claude mcp add flow -- npx @flow-engineer/flow mcp serveAny other MCP host: the command npx, the arguments @flow-engineer/flow mcp serve, started in the
agent’s folder (or add --dir <folder>; --cloud <url> as for the other commands). It speaks
MCP on stdio on @modelcontextprotocol/server, which comes with Flow Framework (0.16.0 on): nothing
more to install, in an agent folder made on 0.15 too.
| Tool | Reads | Key |
|---|---|---|
list_conversations | flow conversations: environment, since, grade, contact, limit (default 20, at most 200) | FLOW_KEY; production: FLOW_LIVE_KEY when set |
show_conversation | flow conversations show <id>: id, environment; each reply’s tool calls (name, ms, ok, error) and delivery (queued, sent, delivered, read, failed with the channel’s code) | FLOW_KEY, then FLOW_LIVE_KEY when FLOW_KEY does not find it; production: FLOW_LIVE_KEY first |
list_issues | flow issues: status (default open) | FLOW_KEY |
list_insights | flow insights: days 7 or 30 (default 7), status open (default), fixed, muted or all | FLOW_LIVE_KEY when set, else a flow_live_ FLOW_KEY |
insight_conversations | flow insights show <finding>: finding, days; each conversation’s window, the 3 messages before the flagged one and 2 after, when Flow sends it | FLOW_LIVE_KEY when set, else a flow_live_ FLOW_KEY |
list_templates | flow templates: status approved (default) or all | FLOW_KEY |
read_logs | flow logs: environment (default staging), since (default 1h, at most 30d), until, severity, grep, limit (default 200, the newest; truncated says when more matched) | CI key |
deploy_status | flow deploy status: environment (default both) | CI key |
- Every tool only reads (
readOnlyHint: true). Nothing here deploys, changes a secret or sends a message. FLOW_KEY: aflow_test_key, the oneflow setupwrites, reads staging only. Production, and Insights (closed production conversations only), read withFLOW_LIVE_KEYwhen it is set: the workspace’s owner makes it withnpx @flow-engineer/flow setup --live(its limits). The tool descriptions repeat its three tiers in the CLI’s words, so a coding agent asks for aflow keys grantonly as its tier allows. Each result carriestext_mode(raw,deidentifiedorwithheld). The CI key,FLOW_FRAMEWORK_CLOUD_KEY(oci_...), reads logs and deploys in both environments.- The keys are read again on every call, from the environment the server started with, then the
folder’s
.env: a key fixed in.envworks on the next call, without restarting the server. No tool returns a key; errors name one by its prefix and last 4 only. - A result is
structuredContent(what the command’s--jsonprints) and the same JSON as text under a one-line summary, such as1 conversation, 1 reply graded wrong. Read one: show_conversation. - A failure is a tool error (
isError: true) whosestructuredContentis--json’s failure document,{ ok: false, error: { code, message, hint? }, next }: a refused key says which key the tool reads, what it found and how to get the right one (a live key refused for something it was not granted: Flow’s hint naming thekeys grant, and the three tiers);no_flow_keyandno_ci_keyhavenextnpx @flow-engineer/flow setup, andno_live_key(a production read with neither key)npx @flow-engineer/flow setup --live. - Message text is as the agent API sends it: de-identified on a live key made without
--content, withheld for accounts that keep it back, and contacts only as a label (Contact 4f1c).
Expected result: /mcp in Claude Code lists flow with eight tools, and “did the booking tool fail
in the last conversation?” runs list_conversations, then show_conversation.
WhatsApp templates
Section titled “WhatsApp templates”Before declaring WhatsApp templates in createAgent, read the approved ones and paste them
(Framework 0.14.7 on):
npx @flow-engineer/flow templates [--all] [--json] # name, language, category, status, send ordernpx @flow-engineer/flow templates --declare # the approved ones as createAgent's templates: [...]- It reads
GET https://flow.engineer/dashboard/v1/agent/templateswith the agent’sFLOW_KEY(Authorization: Bearer;--cloud, elseFLOW_FRAMEWORK_CLOUD, for another Flow). A live key reads the agent’s own WhatsApp numbers’ templates, those made in WhatsApp Manager included; a test key the simulated sender’s. Templates are the business’s own words, so any key of the agent holdingmessaging,tracesorreadreads them: a live key does not needreadhere (its checklist line is “Read WhatsApp templates”). The answer lists every status; the command shows approved ones,--allevery status,--jsonthe server’s answer. --declareprints each approved template exactly as Meta has it (header, body with its numbered or named variables, footer, buttons), then one commentedagent.nudgeper template with the order its values go in, such asbody[0] = {{listing}}, body[1] = {{answer}}.flow testwarns, never fails, for each declared template that is missing, not approved, or differs in its category, header, body, footer or buttons.- A dashboard without the route answers 404:
flow templatessays so (exit 1).
Send Flow feedback
Section titled “Send Flow feedback”When Flow gets in your way, tell the Flow team directly: a command that failed or printed the
wrong next, a guide that was wrong or missing, an error that did not say what to do, or a part
that worked well. POST it as JSON; no key or account is needed.
curl -sS https://api.flow.engineer/v0/feedback \ -H 'content-type: application/json' \ -d '{"feedback": "flow link --channel imessage --json exited 0 but next was empty.", "kind": "bug", "agent": "Claude Code", "flow_version": "0.9.0", "about": "flow link"}'Expected result: 201 with { "id": "fbk_...", "received": true }.
| Field | ||
|---|---|---|
feedback | required | What happened, what you expected, and how to reproduce it. Up to 4,000 characters. |
kind | optional | bug, docs, feature or other. |
agent | optional | The coding agent sending it, such as Claude Code or Codex. |
flow_version | optional | The @flow-engineer/flow version in package.json. |
about | optional | The command, API, page or file it is about. Up to 300 characters. |
- Describe Flow’s behaviour, not the owner’s product: never include keys, tokens,
.envvalues, customers’ data or the owner’s private code. Tell the owner in one line what you sent. - Send one post per problem, once. The sender’s IP address is kept with the feedback for 180 days.
- Errors are the API’s usual shape:
400names the field to fix inerror.param,429means too many posts (10 a minute per address; waitRetry-Afterseconds), and500means it was not stored: try once more a minute later.GETon the same URL describes the fields.
Thanks, we read every comment.