Driving Fused end-to-end (spec-first, agent-authored)
Part of the Fused skill set — don't work from it alone. When the output is a rendered UI (a dashboard or an editable widget), also load
fused-widgets; for a question/approval/plan UI, loadfused-feedback. Seefused-guidefor the full set.
Fused organises work as workspace ⊃ project ⊃ UDF. You — the driving agent — author the specs and the code; Fused supplies the scaffold, the deterministic validators, a spec↔code consistency hook, and the run/deploy machinery. There is no code-generation command and no API key in the loop: authoring code is your job.
Built-in
_coreworkspace. Fused provides a read-only_coreworkspace. Its source trees are no longer bundled in the wheel — they are cloned at runtime from an external git repo into~/.openfused/core/, materializing at~/.openfused/core/skills/<project>/(first boot needsgit+ network). Its projects (e.g.task-management) are available immediately viafused dev serveasworkspace="_core", project="task-management". User projects live in thedefaultworkspace (or any named workspace) —_core(and any name starting with_) is reserved and cannot be created by users.
The full loop:
user request
→ pick an environment (local-first for dev)
→ decompose into projects + UDFs (py data + json widget)
→ write spec.md per UDF → user approves SPECS (never code)
→ author main.py / main.json yourself
→ verify_code → git commit (the pairing hook keeps spec+code in sync)
→ run / preview locally (data, or a rendered widget)
→ view in the flow UI (separate tool)
→ deploy to preview → promote to release (AWS)
The spec is the only artifact the user reviews. Most tasks want a widget back, not raw data — see the fused-widgets skill for the py-UDF-computes → json-widget-renders pattern.
What's MCP vs what's CLI
The whole loop is reachable without a bespoke codegen command — you author files with your editor tools and use these surfaces:
Step Surface Create/select an env CLI ( env create; MCPenv_create/env_updateneed the server started with--enable-infra).list_envs/env_showare read-only MCPCreate a project MCP project_newor CLIproject newWrite spec.md / author code you write the files (no command) Validate code MCP verify_codeor CLIcode verifyCommit (spec+code together) git commit— the pre-commit hook enforces pairingInspect a project / sync manifest MCP project_show(re-syncs[udfs.*]from disk) / CLIproject show(context packet; lists UDFs but does not rewrite the manifest)Run / test code MCP execute_code/test_codeor CLIcode run/code testPreview a widget CLI only ( widget open, the parley) — no MCPDeploy/promote/rollback MCP ( --enable-infra) or CLI
Step 0 — Pick an environment (local-first)
You do not need a cloud account to build and preview. For development, use the local backend (code runs in a project venv on the host):
fused env create dev --backend local
AWS is the production target (deploys UDFs to stable URLs); set it up later via
the fused-setup skill. Resolution: if more than one env exists,
nothing is auto-selected — pin the project (fused project set <project> --env dev) or pass --env/OPENFUSED_ENV. Verify with
get_project_context (environment.resolved_env).
An unpinned project with multiple envs makes
code run/widget-resolve and other commands warn and degrade. Pin early.
Step 1 — Decompose into UDFs
Decide the UDFs. The common shape is one or more py data UDFs + a json
widget UDF on top:
py(scripts/<name>/main.py) — computation/API; returns a value (usually a DataFrame).json(scripts/<name>/main.json) — a JSON-UI widget that visualizes apyUDF's output via the{{ref}}grammar. See fused-widgets.
Where the widget lives decides where it renders. The app's project surface live-renders only saved dashboards at
widgets/<stem>.json; ajsonUDF (scripts/<name>/main.json) is the deployable form and shows as source there. So when the goal is "the human views the dashboard in the flow UI", author the widget aswidgets/<stem>.json— not ajsonUDF. Reach for ajsonUDF only when you need a deployable widget URL (preview it withfused widget open). See fused-widgets › "What a widget is".
Each UDF is one independent capability — lean small. Slugs:
^[a-z][a-z0-9]*([-_][a-z0-9]+)*$, ≤64 chars — - and _ are both accepted as
segment separators, so snake_case names like list_comments are valid
(e.g. sessions, dashboard, list_comments).
Step 2 — Create the project
fused project new taxi-pipeline # or MCP: project_new("taxi-pipeline")
Scaffolds openfused.toml, SKILL.md, and the
scripts/ widgets/ references/ assets/ convention dirs (with scripts/pyproject.toml
and scripts/tests/); on first use it initialises the default workspace as a
git repo with the openfused-managed pre-commit hook.
It also seeds scripts/.venv for you (runs uv sync with the baseline
widget-resolver deps — duckdb/pandas/pyarrow/pyyaml/cryptography) so a fresh
project runs one-shot on the local backend: the first execute_code / code run
/ widget render works without a manual uv sync. Seeding is best-effort — if
uv is missing, OPENFUSED_LOCAL_INSTALLER=pip is set, or uv sync fails (e.g.
no network), the project is still created and project new returns a warning
(CLI stderr / MCP warnings) telling you to seed manually.
If you see a seeding warning (or you need more deps), extend the venv before the run step. Use project add-dep, which runs uv add + uv sync in one step so the venv is never left stale (a bare uv add would trip the stale-venv warning until the next uv sync):
fused project add-dep taxi-pipeline duckdb pandas # UDF-specific runtime deps
fused project add-dep taxi-pipeline pytest coverage --dev # only if you'll run `code test`
(Equivalent manual form: cd …/taxi-pipeline/scripts && uv add … && uv sync. A stale venv left by a bare uv add is auto-reconciled on the next local execute_code/code test anyway — unless OPENFUSED_NO_VENV_SYNC is set — but add-dep keeps it clean up front.)
project new <name>is the simple scaffold.project create <name>is the same scaffold plus--description/--env/--useflags (pin the env and make it active in one shot). Use whichever fits; preferproject newfor the plain case.
Step 3 — Write the specs
For each UDF, write scripts/<name>/spec.md yourself (create the folder if
needed). Fused does not draft specs — you do. A py spec:
# taxi-analysis
Joins NYC taxi trips to zone boundaries and returns mean fare per zone.
## Inputs
- `bucket` (str), `prefix` (str)
## Output
DataFrame: `zone_id`, `zone_name`, `mean_fare`, `trip_count`
## Notes
- Use DuckDB for the join; filter zones with < 10 trips.
A json widget UDF also gets a spec.md (what it shows + which py UDFs it
references). Specs are free-form markdown; the more precise, the better the code.
Step 4 — Get approval on the SPECS (in a widget)
Stop and get the user to review the specs before writing code — and render them in a widget, the same way the UI's spec-review gate does, rather than pasting the spec prose into the chat. Specs are the human-review surface; make that surface a real rendered review, not a wall of text.
Write a spec-review widget and put it in front of the human with fused widget open (the CLI/standalone analog of the architect's ask_user gate).
Mirror what the UI does:
- Render the spec content as
markdown(onemarkdownnode per UDF/widget spec) — proper headings/lists/code, not flattened intotext. On a revision round, render adiffnode instead (before= the previously-approved spec,after= the new one) so the human sees exactly what changed —diffis built for reviewing markdown spec changes. - Add a slim decision control: a
text-input(param: "feedback", "Requested changes (optional)") and two submit buttons — Approve (action: "approve") and Request changes (action: "request-changes").
{
"type": "div",
"props": { "style": "display: flex; flex-direction: column; gap: 16px; padding: 16px" },
"children": [
{ "type": "text", "props": { "value": "Spec review — approve to build, or request changes", "variant": "h3" } },
{ "type": "markdown", "props": { "value": "<the full taxi-analysis spec.md here>" } },
{ "type": "markdown", "props": { "value": "<the full trip-dashboard spec.md here>" } },
{ "type": "text-input", "props": { "param": "feedback", "label": "Requested changes (optional)", "placeholder": "What should change before this is approved?" } },
{ "type": "div", "props": { "style": "display: flex; flex-direction: row; gap: 8px; justify-content: flex-end" }, "children": [
{ "type": "button", "props": { "label": "Request changes", "action": "request-changes", "submit": true, "variant": "secondary" } },
{ "type": "button", "props": { "label": "Approve specs", "action": "approve", "submit": true, "variant": "primary" } }
] }
]
}
widget open blocks until the human submits and returns the final $param state
(the chosen action + any feedback) to you. On approve → author the code
(Step 5). On request-changes (or any feedback) → edit the specs and re-open a
fresh review widget; loop until approved. Summarise if multiple UDFs are pending.
In-app worker exception. An agent spawned by the flow app does this through the teamwork MCP, not
widget open: the architect runs theask_userspec-review gate by authoring ONE complete widget (the plan body astext, adiffnode per changed spec file, and the approve/request-changes buttons — no separatedetails/pathsargs). Thewidget openform above is the CLI / standalone path.
Step 5 — Author the code yourself
Write main.py / main.json into each UDF folder to satisfy its spec. You are
the codegen — there is no udf generate. Iterate by editing the spec and the
code together (keep them consistent — Step 6's hook enforces it).
UDF return form (py):
import fused
@fused.udf
def main(threshold: int = 0):
...
return df # a DataFrame, or a scalar
- Prefer a
@fused.udfentrypoint (takes params; portable; works as a fan-out worker and a served route). A bare top-levelresult = <value>is fine for a quick no-param script. Never both in one file. - A
@fused.udfUDF works as a widget{{ref}}source too (the resolver runs the same form). See fused-execute for libraries/secrets/S3 patterns.
Widget (json): author main.json as a component tree that binds to your
py UDFs with SELECT … FROM {{udf-name?arg=$param}}. See fused-widgets
for the config document, the {{ref}}/$param grammar, and previewing.
Step 6 — Validate, then commit
Validate the code through the deterministic scanners (AST code scanner, dep scanner, input firewall), then commit the spec+entrypoint together:
fused code verify scripts/taxi-analysis/main.py # or MCP verify_code(...)
git -C ~/.openfused/workspaces/default add taxi-pipeline/scripts/taxi-analysis/
git -C ~/.openfused/workspaces/default commit -m "taxi-analysis: spec + impl"
The pre-commit hook blocks one-sided commits — a spec.md without its
entrypoint, or vice versa — so every committed state is internally consistent.
git commit --no-verify is the escape hatch for a genuine one-sided change (e.g.
fixing spec prose with no code change).
UDFs are discovered by directory listing (scripts/<name>/ with main.py or
main.json), so a freshly authored UDF is already visible to run/preview — you do
not need to register it. The manifest's [scripts.*]/kind table is synced by the
MCP project_show tool and at deploy time; the CLI project show returns the
read-only context packet and lists UDFs but does not rewrite the manifest.
archive/— soft-deleted artifacts, never enumerated. A project may carry a top-levelarchive/directory that mirrors the layout (archive/scripts/<udf>/,archive/widgets/<stem>.json,archive/references/<name>.md,archive/canvas.toml). It holds artifacts a human archived in the app — a reversible soft-delete. Because discovery is a shallow, by-name scan of the live dirs, archived artifacts are invisible to everything you drive: they never appear inget_project_context/project show, the pipeline canvas, or deploy, and a deploy of an archived UDF is simply "not found".archive/is git-tracked (provenance travels with the repo) but it is not yours to author into: archive/restore is an app + human operation, not a CLI step, so never write intoarchive/, treat it as a UDF source, or "restore" by hand-moving files. The one place archived source is still read is the widget resolve fallback (see fused-widgets › archived widgets).
work-products.json— the app-owned provenance manifest, read-only to you. A project created/managed in the app carries awork-products.jsonat the project root (besideopenfused.toml, git-tracked, one per project). It is the app/shell's record of each artifact (widget / UDF / reference): itsstatus(draft/active/archived),reviewState, whether it'sisPrimary, asummary, aversion(sha256:of the file's content), and provenance (producedByRunId,taskId,createdBy, timestamps). Treat it exactly likearchive/: the shell is its single writer — never hand-author, edit, or "fix" it, and don't commit a change to it yourself. ThefusedCLI has no command or UDF that writes it, by design. Editing a widget/UDF file leaves that artifact'sversion(and review state) in the manifest stale; that is expected and self-heals on the app side — new CLI-authored files are picked up by the app's catalog auto-adopt, and edits to existing records re-hash on the next app publish. You don't need to read it either:get_project_contextalready surfaces the published-artifact metadata derived from it, so orient from the context packet, not from the raw manifest.
Hard-delete a whole project:
fused project delete <name>. Distinct from the app's soft-deletearchive/, this removes the project from the workspace:git rm -rf -- <name>+ a--no-verifycommit, then cleans gitignored residue (scripts/.venv,__pycache__). It rejects_coreand any underscore-prefixed (reserved) name with aValueError, and prints JSON{name, deleted, root}.
Step 7 — Run / preview locally
Local backend needs a project venv. You should have seeded it back in Step 2
(uv add duckdb pandas && uv sync). If you skipped that, or a UDF you just wrote
imports something new, top it up now before running:
cd ~/.openfused/workspaces/default/taxi-pipeline/scripts
uv add <new-dep> # whatever the py UDFs newly import
uv sync
A missing/incomplete .venv raises a guided error telling you to uv sync.
Run a py UDF (see its output as data):
fused code run scripts/taxi-analysis/main.py --project taxi-pipeline
# or MCP: execute_code(code=<main.py contents>, project="taxi-pipeline")
test_code/code test requires --project on the local backend.
Preview a widget (the result comes back as a rendered widget, not rows). A
saved dashboard (widgets/<stem>.json) renders in the flow UI
(Step 8) or via widget open widgets/<stem>.json. A json UDF
(scripts/<name>/main.json) is not live-rendered on the flow project surface — preview
it with widget open scripts/<name>/main.json or the parley (standing loop). To
self-verify a widget resolves headlessly before showing a human, drive the
resolve daemon — recipe in fused-widgets.
You open the widget — don't tell the user to (CLI / standalone). After authoring and self-verifying the file, run
fused widget open <file>yourself (it launches/reuses the widget-host, opens the browser, and blocks until the human responds, returning their reply). Ending your turn with "open it in the flow UI" strands the human — drive the surface and bring back the result. Exception — an agent spawned by the flow app (an architect/worker run) must NOT runwidget open/the parley (they wait on a human and would hang the run): it writeswidgets/<stem>.json, the flow UI live-renders it, and it asks for feedback viaask_user(the single human-ask tool). See fused-widgets › the decision tree.
Step 8 — View in the flow UI (separate tool, out of scope)
The local web UI is flow — a separate client (fusedio/flow), started with the
flow CLI (or npx @fusedio/flow once published). Its project surface renders a
project's saved dashboards (widgets/<stem>.json) natively and hosts agent runs.
Driving flow is out of scope here — from the CLI, preview widgets with fused widget open (Step 7). A json UDF (scripts/<name>/main.json) shows as source in flow,
not a live render.
Step 9 — Deploy to preview (AWS)
Prerequisites (AWS only): an AWS env with cache_bucket, resolved for this
project, and a provisioned serving plane (fused infra serve). Verify the
resolved env first (fused project show → environment.resolved_env).
fused project deploy taxi-pipeline # all UDFs → preview
fused udf deploy taxi-analysis --project taxi-pipeline
A py UDF deploys to an HTTP route; a json widget UDF deploys to a rendered
widget URL (preview → release). The response echoes env: <name> and a preview
URL per UDF — verify the env matches your intent. Deploy always targets
preview; release moves only via promote/rollback.
Via MCP (--enable-infra): udf_deploy("taxi-analysis", project="taxi-pipeline").
Step 10 — Promote to release
fused project promote taxi-pipeline
fused udf promote taxi-analysis --project taxi-pipeline
Repoints release to whatever commit preview runs; the release URL is stable from
first promote. Roll back with udf rollback … [--to <commit>] (only prior
release commits are valid targets). MCP: udf_promote/udf_rollback
(--enable-infra).
Iterating
Behaviour wrong? Edit spec.md and the code together (both under scripts/<name>/),
re-verify_code, re-run/preview, and commit (the hook keeps the pair consistent).
For widgets, re-open to re-preview. There is no regenerate command — you re-author
the changed UDFs.
Guardrails
Spec-first, agent-authored
The user reviews specs, not code. Author code only after spec approval. Keep
spec.md and the entrypoint consistent — the pre-commit hook enforces it on
commit.
The env is resolved per project — verify before deploying
Every deploy/promote/rollback echoes env: <name>. If it is wrong, pin
(fused project set <project> --env <name>) or override (--env /
OPENFUSED_ENV). With a single env it auto-selects; with several you must pin.
Deploy to preview first; promote to release
The two-channel model exists so code passes through preview before release.
udf deploy --channel release is only for bootstrapping the very first release
URL.
Retiring a UDF
fused udf retire <udf> --project <p> --yes revokes both channel mounts and
drops the UDF from the cloud snapshot (the on-disk folder stays). Destructive
(--enable-destructive on MCP). A UDF present in the cloud snapshot but absent
on disk shows as orphaned in project status.
Charts use fused components — never Vega-Lite / Plotly / matplotlib
A dashboard widget is a {"type": <component>, "props": {…}} JSON file in
widgets/. The type MUST be a built-in fused component — charts are
line-chart, bar-chart, stacked-bar-chart, stacked-area-chart,
scatter-chart, donut-chart, heatmap-chart (plus metric, sql-table,
text, html, inputs, …). Do NOT emit a Vega-Lite / Vega / Plotly /
matplotlib spec or inline a data array — those are not fused components and
render as unknown component: <type>. The supported set is generated from the
widgets package (components.json, the hard type gate); when unsure, read
the component catalog in the fused-widgets skill rather than guessing.
A chart gets its data from a sql prop (DuckDB) that reads a UDF via
{{udf_name}} and aliases the result columns to the chart's contract — e.g.
SELECT month AS label, sum(revenue) AS value FROM {{sales}} GROUP BY 1. The
DuckDB resolver is sandboxed and cannot read files, so every chart needs a
backing UDF in scripts/; it cannot read a references/ file directly. Author
each widget UDF with a udf(...) entry function returning a DataFrame (helper
functions alongside it are fine); on the local backend its imports come from the
project .venv (uv add …).
MCP gating reference
| Operation | MCP tool | Gate |
|---|---|---|
| Create / inspect project | project_new / project_show / list_projects / project_status |
ungated |
| Validate code | verify_code |
ungated |
| Run / test code | execute_code / test_code |
ungated |
| Deploy / promote / roll back a UDF | udf_deploy / udf_promote / udf_rollback |
--enable-infra |
| Retire a UDF | udf_retire |
--enable-destructive |
There is no codegen MCP tool — authoring is Write + verify_code + git +
project_show, all already agent-reachable. Widget preview is CLI/app-only.
See also
- fused-widgets — authoring + previewing widgets (the usual "result").
- fused-execute —
execute_code/code runpatterns (libraries, S3, secrets). - fused-setup — install + AWS env provisioning.
- fused-cli — full command/flag reference and the local-servers inventory.