Migration Guide
This guide migrates an application from Flue 1.0.0-beta.x to the new v2.0.0 preview release. It is written for working beta applications: every section pairs the beta API with its replacement, and the checklist at the end orders the work.
The release is a redesign, not an increment. Five conceptual changes drive almost every mechanical one:
flue build/flue devCLI commands — replaced by Vite with theflue()plugin;vite devandvite buildare the only commands.- The auto-mounted
flue()router and discovery by directory — replaced by explicit routing inapp.ts; the'use agent'scan registers agents. defineAgent(async initializer => config)with a config bag — the agent is the function now: an exported capitalized agent function composing behavior with Flue Hooks;defineAgentis gone entirely.- Workflows (
defineWorkflow,invoke(), runs, run events) — removed. Use awaitedinit()handles, durable tools, or your own orchestrator. - The deployment-wide SDK client (
client.agents.*,client.workflows.*) — replaced by a conversation-scoped client: one client per conversation URL.
Before you start: persisted state resets
The current release stores Flue schema version 7; the beta stored version 5. Pre-1.0 persisted schemas are reset-only — the runtime rejects an older database before any application code runs, and there is no in-place migration.
- If beta conversation state is disposable, plan a drained deployment: retire the old agents (on Cloudflare, with
deleted_classesmigrations) and create fresh ones. Application data that shares an agent’s storage (abase/wrapDO extension, values written beside Flue’s tables) is deleted with it — export anything you need first. - If beta state must survive, export it through the beta application before upgrading, and re-seed after.
Everything else in this guide can be staged; this one is a hard boundary.
Build and dev commands
flue build and flue dev are removed. A Flue application is now a Vite project: add @flue/vite (and on Cloudflare, @cloudflare/vite-plugin) and author vite.config.ts — flue() must come before cloudflare():
import { cloudflare } from '@cloudflare/vite-plugin'; // Cloudflare target only
import { flue } from '@flue/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [flue(), cloudflare()],
});
flue dev --target cloudflare— nowvite dev.flue build --target cloudflare— nowvite build.flue build(Node) — nowvite build, thennode dist/server.mjs.- Target selection via
--target— now auto-detected from the plugin array, or settargetinflue.config.ts.
On Cloudflare the plugin generates two inputs the Cloudflare plugin consumes — .flue-vite/_entry.ts (the Worker entry) and .flue-vite.wrangler.jsonc (your authored wrangler.jsonc merged with generated bindings). Add both to .gitignore. Build output, preview, and deploy belong to @cloudflare/vite-plugin; deploy against the config it emits into dist/.
flue run remains for one-shot local execution, with new flags: flue run <path> --message "..." [--name <agent>] [--id <id>] [--data '<json>']. The beta’s workflow-oriented --input is gone with workflows. The HTTP-based flue run <name> form is gone too — the command takes an agent module path and executes in-process, so --server, --header, --target, --root, --output, and --config have no replacement; to call a deployed server, use the SDK’s conversation client instead. In flue.config.*, the retired root and output fields are rejected by strict validation everywhere except flue run, which silently drops unknown keys — delete them.
Routing: the auto-router is gone
The beta’s flue() router (app.route('/', flue()) from @flue/runtime/routing, or the generated default app) no longer exists. app.ts is now required, and it mounts every route explicitly:
import { flue } from '@flue/runtime/routing';
const app = new Hono();
app.use('/agents/*', requireUser);
app.route('/', flue()); // agents, workflows, channels — discovered and mounted
export default app;
import { createAgentRouter } from '@flue/runtime/routing';
import { Hono } from 'hono';
import { Triage } from './agents/triage.ts';
const app = new Hono();
app.use('/agents/*', requireUser);
app.route('/agents/triage', createAgentRouter(Triage)); // explicit, per agent
export default app;
createAgentRouter(fn)is a pure router factory servingPOST /:id,GET|HEAD /:id,POST /:id/abort, andGET /:id/attachments/:attachmentIdrelative to the mount. URL shapes are yours.- Registration comes from the
'use agent'scan, not the mount. A dispatch-only agent stays unmounted and still works; mounting registers nothing. - Workflow routes (
POST /workflows/<name>,/runs/<runId>) are gone with workflows, as are therunsmodule export andWorkflowRouteHandler/WorkflowRunsHandlertypes.
Defining an agent
The beta’s async initializer returning a config bag becomes a synchronous agent function composing behavior with hooks, in a module marked by the 'use agent' directive:
import { defineAgent } from '@flue/runtime';
export default defineAgent(async ({ id, env }) => ({
model: 'anthropic/claude-sonnet-4-6',
instructions: `Help with ticket ${id}.`,
tools: [lookupOrder],
skills: [refundsSkill],
subagents: [reviewerProfile],
sandbox: bash(myFactory),
cwd: '/workspace',
durability: { maxAttempts: 5 },
}));
'use agent';
import {
type AgentProps,
useModel,
useSandbox,
useSkill,
useSubagent,
useTool,
} from '@flue/runtime';
export function Support({ id }: AgentProps) {
useModel('anthropic/claude-sonnet-4-6');
useSandbox(myFactory, { cwd: '/workspace' });
useTool(lookupOrder);
useSkill(refundsSkill);
useSubagent({ name: 'reviewer', description: '…', agent: Reviewer });
return `Help with ticket ${id}.`;
}
Support.durability = { maxAttempts: 5 };
The agent is the exported function — there is no wrapper and no config bag. Durability became the durability static: assigned on the function like agentName/initialData, so it applies wherever the agent runs (mounted, dispatch-only, start(), flue run) and stays readable when a render crashes.
Field-by-field:
model— nowuseModel(model, options?): required, exactly once per render, root render only.instructions— now the agent function’s return string;useInstruction()appends more.tools— nowuseTool()per tool.skills— nowuseSkill()per skill.subagents(profiles) — nowuseSubagent({ name, description, agent, model?, thinkingLevel? }); the delegate is an agent function, not a profile.thinkingLevel,compaction— nowuseModel(model, { thinkingLevel, compaction }).sandbox,cwd— nowuseSandbox(factory, { cwd }): at most once per render; presence may be conditional.durability— now thedurabilitystatic:Support.durability = { maxAttempts: 5 }. A static, not a hook, because the platform applies it when the function is not running; an environment-dependent policy goes in the assigned expression (flag ? x : y).profile— removed; compose with custom hooks (plain functions calling hooks) instead.actions— removed with Actions. Express reusable operations as tools.description(config) — deleted, no replacement.- initializer
ctx.id— nowAgentProps: the root agent function receives{ id }. - initializer
ctx.env— now platform imports (import { env } from 'cloudflare:workers') orprocess.env; the initializer context is gone. asyncinitializer — the agent function must be synchronous; async work moves into tools, lifecycle hooks (useAgentStart/useAgentFinish), or resource factories such as the sandbox factory’screateSessionEnv().
Rules that have no beta equivalent, because renders repeat:
- The agent function re-renders before every model turn. Resources (
useTool,useSkill,useSubagent) may be conditional — changes are announced to the model asresourcessignals — and so mayuseSandbox(a presence flip swaps the environment at the next turn boundary, announced as anenvironmentsignal),usePersistentState(its storage is keyed by name), and the event hooks (useAgentStart,useAgentFinish,useResponseStart,useResponseFinish— each seam runs whatever the current render declares, at-least-once). The one invariant:useDataWriternames must be declared identically on every render. defineAgentProfileis removed. A subagent is{ name, description, agent }whereagentis a plain agent function rendered per delegation;model/thinkingLevelinherit from the parent unless overridden. Inside a delegate render,usePersistentState,useSandbox,useModel,useDataWriter,useDispatchMessage, and the lifecycle/response hooks throw.- Identity is the exported function’s name, or an
fn.agentName = '...'string-literal static override; PascalCase and lower-kebab-case are both valid, and identities are unique per application. Renaming an agent function without anagentNamepin is a storage-identity change; renaming the file changes nothing.
New capabilities you will likely reach for while migrating — durable per-instance state (usePersistentState), creation data (useInitialData + the initialData schema static), the delivered-message cursor (useDelivery), self-dispatch (useDispatchMessage), client-facing data parts (useDataWriter), lifecycle seams (useAgentStart/useAgentFinish), and response metadata (useResponseStart/useResponseFinish).
Tools
The tool contract keeps defineTool({ name, description, input, output, run }), with one rename and two new flags:
run({ input })→run({ data }). The parsed-arguments field onToolContextis nowdata;signalis unchanged andlog(aFlueLogger) is always present. The pre-betaparameters/executemarkers still throw.harness: truereplaces session plumbing: the tool receivesharness(harness.prompt()for model calls in the tool’s own scratch conversation,harness.sandboxfor the environment).harness.session()andFlueSession/FlueSessionsare gone —prompt()lives directly on the harness, andsession.task()delegation is now the model-driventasktool overuseSubagentdeclarations. Thetasktool’sagentparameter is required, and an unnamed task no longer clones the parent’s configuration — declareuseSubagent(GeneralSubagent)for a blank fresh-context delegate (see Subagents).durable: trueopts a tool into checkpointed execution:runreceivesstep, side effects go throughstep.do(name, fn), and recovery replays recorded step values instead of re-running them. This is the in-agent replacement for small workflow orchestration.
harness.fs is also gone: the harness exposes harness.sandbox, a SessionEnv carrying exec, the file verbs, cwd, and resolvePath. Adapters may not support every verb and may expose native accessors (for example Cloudflare Shell’s shellWorkspace(harness.sandbox)).
Skills and markdown imports
Import-attribute syntax (with { type: 'skill' } and friends) is removed; the specifier decides:
- An import that resolves to a
SKILL.mdpackages the whole skill directory and returns aSkillReferenceforuseSkill(). - Any other
.mdimport is plain markdown text (a string), inlined at build time. To make one a skill, pass it throughdefineSkill({ name, description, instructions })—defineSkillwrites frontmatter itself, so the file stays plain markdown. (A?skillimport query was never released; do not use one.) - Vite-native queries (
?raw,?url) keep their usual meanings.
Workflows are removed
defineWorkflow, invoke(), listRuns(), getRun(), workflow HTTP routes, client.workflows.*, useFlueWorkflow(), the src/workflows/ discovery directory, the Workflow API, and workflow run events are all gone. There is no framework job abstraction to migrate to — pick the smallest replacement that preserves your semantics:
-
A single model operation with a returned value (the common beta workflow): an awaited handle.
init(agent, { id })addresses an instance;await handle.dispatch(message)delivers through the normal queue, waits for settlement, and resolves with the reply (text,data,metadata,submissionId). A failed or aborted run rejects withAgentRunError.import { init } from '@flue/runtime'; import { Summarizer } from './agents/summarizer.ts'; const reply = await init(Summarizer, { id: `summary-${caseId}` }).dispatch(text); return reply.data.summary; -
Checkpointed side-effect sequences inside an agent: a
durable: truetool withstep.do(...). -
Multi-step orchestration with its own durability, retries, and inspection (what workflow runs gave you): an application-owned orchestrator. On Cloudflare, use a Cloudflare Workflow whose steps call
init(...).dispatch(...)— one agent send per step, with the recorded step result standing in for the reply on re-execution. Run inspection (getRun()) has no framework replacement: reconcile from your orchestrator’s own state and fromsubmission_settledobservability events.
In standalone Node scripts (cron jobs, CI, tests), boot the runtime first with start() from @flue/runtime/node, passing agent functions (or { agent, name? } entries); then init()/dispatch() work as they do in a server. See Standalone scripts.
Dispatch and conditional sends
dispatch(agent, request) keeps its shape with two changes:
- The creation seed field is
initialData(validated by the agent’sinitialDataschema static at creation, read withuseInitialData(); ignored on sends that continue an existing instance). If your beta app abused the first message body to carry setup facts, move them here. uidsend conditions: omit to continue-or-create; pass a previous receipt’suidto continue only that incarnation (AgentInstanceNotFoundError/404 otherwise); passnullto create only (AgentInstanceExistsError/409 otherwise, carrying the existing uid). Conditions are checked at admission and create nothing on failure.getAgentInstance(agent, id)looks up{ id, uid }without sending.
A bare string is user-message shorthand everywhere a message is accepted. dispatch() remains fire-and-forget at durable admission; the awaited form is the init() handle above.
Observability
Run-scoped events are gone with workflows; agent activity is observed directly. Register observe(...) as before, and migrate event handling:
run_start/run_end— nowagent_start/agent_end.runIdcorrelation — nowinstanceId(the agent instance),submissionId(one delivered submission), anddispatchId(one dispatch delivery).- Polling
getRun()for the outcome — nowsubmission_settledevents (terminal outcome of recovered submissions) plus your own orchestrator’s state. - Failed run inspection — now
operationevents withisError, carrying the failing operation kind. createOpenTelemetryObserver()from@flue/opentelemetry— nowcreateOpenTelemetryInstrumentation(), registered withinstrument(...)instead ofobserve(...); theexportContentoption became the instrumentation-widecontentpolicy, and the old custom model/tool content attributes are no longer emitted alongside the standard fields.
See the Events Reference for the full envelope (v: 3) and payload contract.
SDK
The beta’s deployment-wide client is now conversation-scoped: construct one client per conversation URL — the agent’s mount URL plus the conversation id. There is no baseUrl, no agent-name addressing, and no client.agents/client.workflows namespaces.
// Beta
const client = createFlueClient({ baseUrl: '/api' });
await client.agents.send('support-assistant', ticketId, { message });
await client.agents.abort('support-assistant', ticketId);
// Now
const conversation = createFlueClient({ url: `/api/agents/support-assistant/${ticketId}` });
await conversation.send({ message, initialData });
await conversation.abort();
The conversation client exposes send (202 admission; returns uid and submissionId), wait(admission), observe(), history(), abort(), and attachmentUrl(). abort() aborts the conversation’s in-flight and queued work — there is no per-submission abort — so shared conversations (an operator chat that also receives dispatched internal work) should account for that scope.
React
@flue/react now exports only useFlueAgent. FlueProvider and useFlueWorkflow are removed.
// Beta
<FlueProvider client={deploymentClient}>…</FlueProvider>;
const agent = useFlueAgent({ name: 'support-assistant', id });
// Now — pass the conversation URL, or a memoized conversation client
const agent = useFlueAgent({ url: `/api/agents/support-assistant/${id}` });
const agent = useFlueAgent({ client }); // useMemo the client — a new instance replaces the session
Messages remain Flue-owned parts-based values; new part kinds (data-* from useDataWriter, message metadata from the response hooks) should be narrowed, not assumed. refresh() and the dormant-when-url-omitted behavior carry over.
Cloudflare deployments
FlueRegistryis gone. The beta’s deployment-wide registry DO indexed workflow runs; nothing replaces it. Append adeleted_classesmigration for it, and for everyFlue<Name>Workflowclass.- Generated classes are per-agent only:
export function Triage()→ classFlueTriageAgent, bindingFLUE_TRIAGE_AGENT(one class per agent function; a file can carry several). Migration history stays user-authored — adding an agent is always the triple: the exported agent function in a'use agent'module, the mount (unless dispatch-only), and anew_sqlite_classesentry. Renames userenamed_classes— but remember the schema reset: a beta-era database is rejected even under a renamed class, so beta agents are usually retired (deleted_classes) in favor of fresh identities. - Your authored
wrangler.jsoncis never modified; the build merges it into.flue-vite.wrangler.jsoncand the deployable config lands indist/. Deploy withwrangler deploy --config dist/<worker>/wrangler.json. - The generated entry exports every agent class plus your
app.tsfetch handler; application-owned exports (your own DOs, Workflows) come fromcloudflare.ts.
CLI
flue init, flue add, flue update, and flue docs remain. flue dev and flue build are removed (Vite owns both). flue run <path> executes one agent module directly — --message, --name (select one agent by name when the module defines several), --id, --data (creation data), --uid/--new, --json. Durability comes from the agent’s own durability static, as everywhere else.
Migration checklist
- Pins. Replace
@flue/*@1.0.0-beta.xwith the current versions; add@flue/vite,vite, and (Cloudflare)@cloudflare/vite-plugin. Drop beta-era patches and vendored builds — re-verify each patched behavior against the new runtime before porting anything. - Build. Author
vite.config.ts(flue()beforecloudflare()); move package scripts tovite dev/vite build; gitignore the generated files. - Routing. Author explicit mounts in
app.ts; deleteflue()router usage; decide which agents are dispatch-only. - Agents. Convert each initializer to an exported capitalized agent function in a
'use agent'module: hooks for behavior, statics (agentName,initialData,durability) for the contract,AgentPropsfor the id, platform env instead ofctx.env. Convert profiles touseSubagentagent functions. - Tools. Rename
run({ input })torun({ data }); adoptharness: truewhere tools prompted sessions; considerdurable: truefor side-effect sequences. - Skills. Delete import attributes; let
SKILL.mdimports package themselves; wrap other markdown withdefineSkillwhere needed. - Workflows. Replace each with the smallest fit: awaited
init()handle, durable tool, or an application-owned orchestrator. - Observability. Migrate
run_*handling toagent_start/agent_end/submission_settledand theinstanceId/submissionId/dispatchIdcorrelation fields. - Clients. Move SDK and React usage to conversation-scoped clients and
useFlueAgent({ url | client }). - Deployment. Append
deleted_classesforFlueRegistryand workflow classes; addnew_sqlite_classesfor new agents; plan the drained deployment for the schema reset. - Verify. Typecheck, tests, a production
vite build, and a check of the built artifact (exports, merged wrangler config) before deploying.