Models and configuration
Models & optionsChoose a model and configure your messaging agent's runtime behavior.
Configure the model and createAgent options in src/agent.ts. The starter already connects
the agent to its tools, test people and hosting environment.
Models
Section titled “Models”openAICompatible works with OpenAI-compatible APIs. With no explicit apiKey or baseURL,
it uses the first non-empty key in this order:
| Order | Environment variable | Provider |
|---|---|---|
| 1 | OPENROUTER_API_KEY | OpenRouter |
| 2 | OPENAI_API_KEY | OpenAI |
| 3 | ANTHROPIC_API_KEY | Anthropic |
| 4 | GEMINI_API_KEY | Gemini |
| 5 | FLOW_KEY | Flow’s hosted models |
With no key, it stops immediately and names the supported variables.
import { openAICompatible } from "@flow-engineer/flow";
const model = openAICompatible({ model: process.env.MODEL });import { openAICompatible } from "@flow-engineer/flow";
const model = openAICompatible({ baseURL: "http://localhost:8000/v1", model: "your-model",});Use your local server’s address and model id in the second example. A server that needs a key
can also receive apiKey explicitly.
MODEL sets the default model id. The starter passes CAFE_MODEL when set. Otherwise each
provider has a default in
src/model.ts.
Set contextTokens to tell Flow the model’s context-window size.
Use Flow’s model key
Section titled “Use Flow’s model key”flow setup writes FLOW_KEY, which can use the account’s $2 of trial model credit. A provider
key takes priority over it. flow status reports the remaining credit without printing the key.
flowModel({ model }) explicitly selects Flow’s hosted models; its default is
anthropic/claude-haiku-5.5. GET /models on the configured Flow model server lists the models
it offers. Set FLOW_MODEL_URL to use another Flow model server.
Rotation and exhausted trial credit
flow setup --rotate key replaces FLOW_KEY. The old key works for another 24 hours;
--now ends it immediately. Neither command prints the full key.
Exhausted credit returns trial_credit_used with a next step. Add your own provider key to
continue with that provider.
Transcribe a voice note
Section titled “Transcribe a voice note”For a local audio file named voice.ogg:
import { readFile } from "node:fs/promises";import { flowTranscribe } from "@flow-engineer/flow";
const audio = await readFile("voice.ogg");const { text } = await flowTranscribe(audio, { filename: "voice.ogg", mimeType: "audio/ogg",});console.log(text);This uses the same Flow key and model-server address. It accepts a Blob, File, Buffer,
Uint8Array or ArrayBuffer, up to 15 MB. language is optional and detected when omitted.
Errors include a message, next step, code and HTTP status.
createAgent options
Section titled “createAgent options”The tables below cover common options. The TypeScript
AgentOptions definition
is the complete contract.
Instructions and tools
Section titled “Instructions and tools”| Option | Purpose |
|---|---|
dir | Folder containing AGENTS.md and skills/ |
files | Supply those files as text when there is no folder |
model | A Model, usually from openAICompatible |
tools | Your tool definitions |
inlineSkills | Include every skill in the prompt rather than loading on demand |
Channels and identity
Section titled “Channels and identity”| Option | Purpose |
|---|---|
channels | whatsapp(), sms(), customUI() or provider channel definitions |
resolve | Resolve a channel identity to your account key |
testPeople | People the playground may play, as [{ name, phone }] |
profile | Account data fetched on demand through get_user_profile |
context | Small facts added before every turn; keep slow lookups out of it |
timeZone | The business time zone, used when the person’s is unknown |
quietHours | Hours to hold business-started messages; false disables them |
transcribe | Speech-to-text for incoming voice notes |
See messaging and people for identity and time-zone behavior, and reminders and nudges for delivery rules.
State and hosting
Section titled “State and hosting”| Option | Purpose |
|---|---|
database | Durable storage; defaults to an in-memory SQLite database |
store | Supply your own store instead of a database |
records | Collections of the agent’s own small records, kept in the same store (records) |
wakeUps | false removes the model’s wake_me_up and cancel_wake_up tools and their list from its prompt; wake-ups set by code still run (wake-ups) |
host | Hosting options passed as host: options.host from buildAgent |
routes | Your HTTP handlers, keyed by path |
events | Event sinks receiving runtime events |
traces | Trace export settings; defaults come from the environment |
Self-hosting covers persistence and restart handling. See Deployment for Flow hosting and Traces for observability.
Turn timing and history
Section titled “Turn timing and history”| Option | Default | Purpose |
|---|---|---|
debounceMs | 300 | Quiet time after a message before a reply starts |
deadlineMs | 60,000 | How long a reply waits for a tool before moving it to the background |
partialAfterMs | 5,000 | How long a finished tool result waits for other tools in that response |
maxSteps | 8 | Model steps per turn |
history.maxTokens | 60% of contextTokens, else 50,000 | History carried into each model call |
history.toolResultChars | 12,000 | Limit for a tool result in history |
Durations are milliseconds. A tool’s own deadlineMs overrides the agent’s value for that tool.
Message hooks and templates
Section titled “Message hooks and templates”| Option | Purpose |
|---|---|
beforeSend | Change or refuse the final message before validation, sending and recording |
intercept | Answer an incoming message without a model call |
templates | Your declared WhatsApp templates |
fallbackTemplate | The template used when a nudge meets a closed WhatsApp window |
shareUpdates | Enable, disable or customize live status guidance in web and app interfaces |
beforeSend return values
beforeSend(message, { channel, address, info, origin }) runs for messages, carousels and
templates on every channel. Return nothing to send the input, another message to replace it,
or { refuse: "why" } to tell the agent to try again.
It runs before platform content checks. History, transcripts and test judges see what was actually sent.
An edit’s new text runs through it too, as a message with only text, and
context.edit.messageId names the message being edited. A replacement is what goes out; a
refusal sends nothing.
Edits, unsends and message ids
On channels whose info has edits and unsends (Telegram and iMessage on Flow Messaging, or
a custom UI with editMessage and unsendMessage), the agent is offered edit_message
({ message_id, text }) and unsend_message ({ message_id }). Its send results there name
each message ({"delivered":true,"channel":"imessage","message_id":"msg_..."}). WhatsApp and
SMS offer neither tool.
The platform decides how late an edit or unsend may come. Flow Messaging refuses a late one
when it is made (422 unsupported_content with edit_window_closed, unsend_window_closed,
first_message_not_editable or not_on_sms), and the agent gets that refusal as its tool
result, for example Not edited: unsupported_content (edit_window_closed, on imessage): iMessage allows edits within 15 minutes of sending; .... Nothing is sent instead.
Your own code gets the id too: a send’s SendReceipt.messageId, the messageId on
message.out events, and a sent nudge’s NudgeResult.messageId. message.edited and
message.unsent events record each change. Requires @flow-engineer/flow 0.7.0 or later.
intercept and keyword messages
intercept(conversationId, text, message) sees the incoming channel and any STOP, START or
HELP classification in message.keyword.
Its answer is stored in history and the transcript. STOP and START are enforced even if the hook returns an answer or throws. See keyword handling.
Thanks, we read every comment.