Migration Guide
This guide migrates a Flue codebase from 1.0.0-beta.9 to Flue 2. 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 breaking version upgrade and major redesign of core internal architecture. Five conceptual changes drive the upgrade:
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 Agent Hooks;defineAgentis gone entirely.- Workflows (
defineWorkflow,invoke(), runs, run events) — removed. Use awaitedinit()handles, durable tools, or your own orchestrator. See Workflows. - The deployment-wide SDK client (
client.agents.*,client.workflows.*) — replaced by the Flue Agent SDK’s conversation-scoped client: one client per conversation URL.
This guide maps old code onto new APIs; it does not teach the new APIs. Read Agents, Agent Hooks, Routing, and Workflows first — the sections below assume you know what the replacements are and only cover what to change.
Before you start: persisted state resets
The current release stores Flue schema version 8; 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 vite, @flue/vite, and hono as dependencies (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— nowvite dev(the dev server moves from port 3583 to Vite’s 5173).flue build— nowvite build; on Node, thennode dist/server.mjsas before.- Target selection via
--target— now auto-detected from the plugin array, or settargetinflue.config.ts.
Update package.json scripts and every CI pipeline that builds the project. On Cloudflare the plugin generates two inputs the Cloudflare plugin consumes — .flue-vite/ (the Worker entry) and .flue-vite.wrangler.jsonc (your authored wrangler.jsonc merged with generated bindings). Add both to .gitignore.
If you have a flue.config.ts, two changes:
defineConfignow comes from@flue/runtime/config; the@flue/cli/configsubpath is gone.- The
rootandoutputfields are retired — Vite owns both. Strict validation rejects them everywhere exceptflue run, which silently drops unknown keys — delete them.
.env loading also changed: vite dev loads Vite’s standard .env files, and flue run loads .env (pass --env for an alternate). Built servers never load .env.
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';
import { channel } from './channels/slack.ts';
const app = new Hono();
app.use('/agents/*', requireUser);
app.route('/agents/triage', createAgentRouter(Triage)); // explicit, per agent
app.route('/channels/slack', channel.route()); // explicit, per channel
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 — keep the old/agents/<name>paths if deployed clients address them.- Registration comes from the
'use agent'scan, not the mount. A dispatch-only agent stays unmounted and still works; mounting registers nothing. - The agent-module
export const routeandexport const attachmentsconventions are deleted. Per-agent middleware becomes ordinary Hono middleware registered before the mount; attachment download exists on every mounted agent. - The
POST /:idbody changed:{ "message": "...", "images": [...] }becomes a bare message object —{ "message": { "kind": "user", "body": "..." } }— with optional top-levelinitialDataanduid. The?waitquery is gone; clients follow the returnedstreamUrlor use the SDK’swait(). - 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, no config bag, and no default export. Discovery no longer cares about the src/agents/ directory: the build scans the source root for the 'use agent' directive, and every exported capitalized function in a marked module is an agent. A converted module without the directive is silently not an agent.
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.defineAgentProfileis removed;defineSubagent()defines delegates shared across agents.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 (harness: truefor app-driven model work).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’screateSandbox().
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. - 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. The beta’s filename-derived identities do not need to be preserved — beta storage cannot carry forward through the schema reset anyway — so pick good function names now and keep them stable. - If the beta agent parsed setup facts out of its conversation
id(order numbers, channel refs), move them to creation data: declare a schema with theinitialDatastatic, passinitialDataat dispatch, and read it withuseInitialData().
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 a new return shape, one rename, and two new flags:
run()returns a result envelope:{ output?, terminate? }. Where a tool returned a bare value, return{ output: <value> }. Returning a barestringstill works — it is sugar for{ output: <string> }— and returning nothing is still allowed for tools without anoutputschema, but any other bare value (object, number, boolean, array,null) now throws at runtime with instructions to wrap it. The optionalterminate: truesibling ends the agent’s turn once the current tool batch settles — the same loop-ending contract the built-infinishtool uses.run({ input })→run({ data }). The parsed-arguments field onToolContextis nowdata;signalis unchanged, andlog(aFlueLogger) andtoolCallIdare 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.
MCP servers are new in this release — useMcpConnection(...) mounts a remote server’s tools; nothing migrates.
harness.fs is also gone: the harness exposes harness.sandbox, a Sandbox carrying exec, the file verbs, cwd, and resolvePath. Adapters may not support every verb and may expose native accessors (for example Cloudflare Computer’s computerWorkspace(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.
Manual invocation (session.skill(...)) is gone with the session surface; steer activation by naming the skill in instructions or in harness.prompt() text.
Sandboxes
- There is no implicit environment. The beta gave every agent an in-memory virtual sandbox by default; now an agent without
useSandbox()has no filesystem and noread/write/edit/bash/grep/globtools, andharness.sandboxthrows. Agents that relied on the implicit workspace attach one explicitly — addjust-bashto your dependencies and declareuseSandbox(bash(() => new Bash({ fs: new InMemoryFs() }))). Agents that never touched files need nothing. sandbox:andcwd:config becomeuseSandbox(factory, { cwd }), as above.- Create remote providers lazily inside the factory’s
createSandbox(options), not at module top level —options.idcarries the conversation id there, which is also how durable per-conversation workspaces work. The beta’s eagerawait Sandbox.create()at module scope must move inside. - The standard tool set is composable: a
SandboxFactorymay passtools: [createReadTool(), createBashTool(), ...]to swap or drop the sandbox-backed set, andbash(factory)wraps a just-bash instance into the virtual sandbox.
See Sandboxes.
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;handle.dispatch(message)delivers through the normal queue and resolves with a receipt at admission, andhandle.read(receipt)waits for settlement and resolves with the reply (text,data,metadata,submissionId). A failed or aborted run rejectsread()withAgentRunError.import { init } from '@flue/runtime'; import { Summarizer } from './agents/summarizer.ts'; const summarizer = init(Summarizer, { id: `summary-${caseId}` }); const receipt = await summarizer.dispatch(text); const reply = await summarizer.read(receipt); return reply.data.summary;A
signalon the beta workflow call aborted the run itself; on the handle,read(receipt, { signal })cancels only the local wait — the submission keeps running and spending. Carrying the option over mechanically converts a durable abort into a local cancel. When cancelling the read should also stop the run, callabort(). -
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 the
init()handle: one step callsdispatch(...), with the recorded receipt standing in for that step’s result, and a following step callsread(receipt), with the recorded reply standing in for its own result on re-execution. Run inspection (getRun()) has no framework replacement: reconcile from your orchestrator’s own state and fromsubmission_settledobservability events.
The retry policy that lived on the workflow moves to the agent’s durability static. Scheduled workflows follow the same move: a cron trigger now dispatches a signal message to an agent (dispatch(Agent, { id, message: { kind: 'signal', ... } })) instead of calling invoke() — see Schedules.
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. All of these replacement patterns are covered in depth in the Workflows guide.
Dispatch and conditional sends
dispatch(agent, request) keeps its shape with three changes:
- The named-string form (
dispatch({ agent: 'name', ... })) is removed — pass the agent function itself. - 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 top-level function and the init() handle’s dispatch() share the same contract; read(receipt) is what awaits settlement, as shown above.
The dispatchId → submissionId rename. Pre-release builds used dispatchId for dispatch receipts and event correlation; the final vocabulary is one name. DispatchReceipt.dispatchId is now submissionId, FlueEvent carries no dispatchId field — submissionId alone identifies a submission’s activity, dispatched or direct (see the Events Reference) — and the telemetry adapters emit flue.submission.id instead of flue.dispatch.id. New submission ids are sub_-prefixed; ids are opaque — do not parse the prefix.
Channels
Every channel connector package changed the same way:
- Mount explicitly. Channels are no longer auto-served from
src/channels/. Each connector now exposeschannel.route(); mount it inapp.tsat the old auto-mount path (app.route('/channels/slack', channel.route())) so registered webhooks keep working. conversationKey→instanceId.channel.conversationKey(ref)is nowchannel.instanceId(ref)andparseConversationKey()isparseInstanceId(), across all connectors, with no aliases; error classes renamed to match (Invalid<Channel>InstanceIdError). Zendesk’sticketKey/parseTicketKeykeep their names.- Most connectors now pass structured facts as
initialDataat dispatch instead of encoding them in the id — preferuseInitialData()over id parsing in channel agents;parseInstanceId()remains as an escape hatch.
Hand-written channels build on the new createChannelRouter(routes) from @flue/runtime — see Channels.
Database
- Ecosystem adapters now take your driver instead of a connection string:
postgres(process.env.DATABASE_URL!)becomespostgres({ query, transaction, close })wrapping your ownpgpool. The same runner pattern applies to libsql, mysql, mongodb, and redis; each ecosystem database page shows the wrapper. db.tsmoved from.flue/db.tsto the source root (src/db.ts), matching the general source-root rule. Standalonestart()scripts takedb:directly and do not readdb.ts.- Custom adapters:
RunStoreandEventStreamStoreare deleted,AgentSubmissionStoregrew settlement and lease methods, and@flue/runtime/test-utilsnow ships contract test suites to verify an adapter against the new obligations.
Providers
Flue’s provider registration schema is gone; providers are now Pi’s own objects, registered with setProvider(). registerProvider(), registerApiProvider(), ProviderRegistrationError, and the registration option bag are removed.
registerProvider('ollama', { api, baseUrl, ... })— nowsetProvider(createProvider({ id: 'ollama', auth, models, api }))with Pi’screateProvider. Models are declared as fullModelobjects (each carries its ownbaseUrland metadata); there is no catalog hydration or zero-fill for custom providers. The Ollama recipe is the template.registerProvider('anthropic', { baseUrl, apiKey })(patch a built-in) — now register your own provider under the built-in’s ID, reusing its catalog models:models: anthropicProvider().getModels().map((m) => ({ ...m, baseUrl })). The gateway recipe shows the full shape.apiKeyon a registration — now the provider’s ownauth.apiKey.resolve()(a fixed value, an env read via Pi’senvApiKeyAuth, or a dynamic exchange). Environment-variable resolution for built-ins is unchanged.contextWindow/maxTokens/reasoning/inputand the per-modelmodelsmap — now fields on theModelobjects your provider declares.headers— nowheaderson theModelobjects, or returned fromauth.apiKey.resolve().storeResponses— removed; no replacement. Open an issue if you relied on OpenAI-hosted item persistence.telemetryoverrides — removed; observability events report the fixed provider-ID normalization only.registerApiProvider({ api, stream, streamSimple })— now pass the{ stream, streamSimple }pair ascreateProvider()’sapifield; the global wire-protocol registry is gone.registerProvider('cloudflare', { api: 'cloudflare-ai-binding', binding, gateway })— nowsetProvider(cloudflareBindingProvider({ binding, gateway }))from@flue/runtime/cloudflare/workers-ai. The generated Worker entry registers it when theprovidersconfig is omitted or lists'cloudflare', and anapp.tsregistration still wins.- In tests, Pi’s compat
registerFauxProvider(...)— nowfauxProvider(...)from@earendil-works/pi-aiplussetProvider(faux.provider); there is no.unregister().
New in the same release: the providers config on the flue() plugin selects which providers ship in the build (flue({ providers: ['anthropic'] })); omitted means all, as before. The list is exhaustive — on the Cloudflare target it includes the Workers AI binding provider, so name 'cloudflare' when you use cloudflare/... models.
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) andsubmissionId(one submission, dispatched or direct).- Polling
getRun()for the outcome — nowsubmission_settledevents (the terminal outcome of every submission) 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.
Agent 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/client.runs 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; returnsuidandsubmissionId),wait(admission),observe(),history(),abort(), andattachmentUrl(). wait()now rejects withFlueExecutionErroron failure or abort; error envelope codes changed (agent_not_found→agent_instance_not_found, plusagent_instance_existsfor conditional sends).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.- Live updates default to SSE with long-poll fallback.
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 the generated, gitignored.flue-vite.wrangler.jsonc, andvite buildwrites the finalized config intodist/with a deploy redirect — deploy with plainwrangler deployfrom the project root, no--configflag. cloudflare.tsmoved from.flue/cloudflare.tstosrc/cloudflare.ts. The generated entry exports every agent class plus yourapp.tsfetch handler; application-owned exports (your own DOs, Workflows, thescheduledhandler) come fromcloudflare.ts, and scheduled work starts withdispatch(), notinvoke().- Update
run_worker_firstfrom the beta’s["/api/*", "/_flue/*"]to cover your actual mounts, for example["/api/*", "/agents/*", "/channels/*"]. - The minimum
compatibility_dateis2026-04-01, validated at build.
CLI
flue init, flue add, flue update, and flue docs remain (flue init is now a full interactive project scaffold rather than a config-file writer). flue dev and flue build are removed (Vite owns both). flue run no longer talks to a built server; it executes one agent module in-process:
# Beta
flue run support --target node --input '{"ticket": 42}'
# Now
flue run src/agents/support.ts --message "Handle ticket 42." --id ticket-42
- Required: the module path and
-m/--message.--nameselects among multiple agents in one module. --input→--data '<json>'(creation data, validated by theinitialDatastatic).- Gone with the HTTP form:
--server,--header,--target,--root,--output,--config, and workflow names. To call a deployed server, use the SDK’s conversation client instead. - New:
--uid/--newsend conditions and--json(result envelope). Stdout is the reply only; logs go to stderr. flue runnever loadsapp.ts— register providers in the agent module if you rely onsetProvider()at app startup.
Migration checklist
- Pins. Replace
@flue/*@1.0.0-beta.xwith the current versions; addvite,@flue/vite,hono, 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; fixflue.config.ts(@flue/runtime/config, noroot/output); gitignore the generated files. - Routing. Author explicit mounts in
app.ts; deleteflue()router usage; mount each channel’sroute(); 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. Agents that used the implicit virtual sandbox declare one:useSandbox(bash(() => new Bash({ fs: new InMemoryFs() }))). - 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. - Channels and database. Rename
conversationKey/parseConversationKeytoinstanceId/parseInstanceId; rewrite database adapters around your own driver; movedb.tsto the source root. - Providers. Replace
registerProvider()/registerApiProvider()calls with Pi’screateProvider()+setProvider()(add@earendil-works/pi-aito your dependencies); replace the Cloudflare binding registration withcloudflareBindingProvider()from@flue/runtime/cloudflare/workers-ai; optionally narrow the shipped providers withflue({ providers: [...] })(name'cloudflare'to keep Workers AI). - Observability. Migrate
run_*handling toagent_start/agent_end/submission_settledand theinstanceId/submissionIdcorrelation 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.