Flank
Flank
Responsibility
Flank is the visual workflow editor for the brain-owned workflow engine. It is the React/ReactFlow canvas where an admin builds and edits the node graphs that brain executes — adding nodes, wiring edges, configuring node inputs, running test executions, and inspecting per-node logs.
Flank no longer owns workflow data or execution. Under T-Bone, the workflow engine and storage moved into brain: brain holds workflows, subworkflows, contracts, adapters, and executions, and serves them over an HTTP API. Flank is re-pointed to read and write brain over HTTP — it is a thin authoring surface on top of brain, alongside the other surfaces (fennec, brisket, the agent tools).
The editor’s long-term home (remain in
apps/flankor fold into fennec) is an open decision. Today it lives inapps/flank/app.
Runtime
Flank runs on TanStack Start / Vinxi with ReactFlow in the UI (port 3100). Its TanStack Start server functions call brain’s HTTP API for all workflow data — list/get/save workflows, fetch a workflow contract, execute or sandbox a workflow, and read executions with their per-node traces. The brain client lives in apps/flank/app/lib/brain-http-client.ts.
Clerk provides authentication; brain authorizes workflow operations behind the ADMIN role (one carve-out: executing published MEDIA_GENERATION/CREATOR_UTILITY workflows is open to CREATOR and above).
Legacy, retired (removed): the original model — workflow/adapter/secret/execution records stored in sirloin via the gRPC
FlankStorageService, and executions run by flank’s own in-process engine overFlankExecutionService— has been fully removed. Theproto/flank/v1package, flank’s server-side engine (apps/flank/server/engine/,grpc-server.ts), and sirloin’s flank storage/MCP surfaces are deleted; flank’s server is now only the host for the editor UI and its TanStack Start server functions (apps/flank/server/entry.ts). All workflow operations go through brain’s HTTP API.
What it is used for
- Authoring workflows and subworkflows: drop nodes from the palette, connect them into a graph, and configure each node’s inputs (including
{{nodes.X}}/{{trigger.X}}references and{{= ...}}expressions — see Workflow Template Expressions for the syntax, the editor’s completion and the publish-time checks). - Multi-node editing: select, copy/cut, and paste a chunk of a graph across workflows.
- Testing: run a draft as a sandbox execution (no published version required) and inspect the per-node execution trace (status, timing, inputs, outputs, errors).
- Publishing: promote a draft to a numbered, immutable version that external executors may use.
See the T-Bone glossary for the canonical definitions of Workflow, Subworkflow, Node, Engine, Execution, and Contract.
Getting started: a media-generation workflow
This is the end-to-end mental model for building a workflow that produces a piece of media (an image, a video, or a carousel) a user can generate.
1. Workflow purpose — what the workflow is for
Every workflow has a purpose:
MEDIA_GENERATION— the workflow is a user-facing generation type. Only these are picked up by sirloin’s generation catalog and shown to end users in brisket; each one becomes a “thing a user can generate.”GENERAL_PURPOSE(the default) — an internal/utility workflow. It runs and is editable, but it is not surfaced as a generation type to users. Use it for building blocks and experiments.CREATOR_UTILITY— an internal helper that CREATOR-level (and above) users may run directly, e.g. the fennec workflow generator’s variables extraction. LikeGENERAL_PURPOSEit is never surfaced as a generation type and never creates a Generation; unlike it, brain lets non-admins execute the published version (sandbox/draft runs stay admin-only).
So the first decision is: set the purpose to MEDIA_GENERATION if you want this flow to appear as a generation option.
2. output vs media_output — how the result leaves the graph
Both are terminal nodes, but they do very different things:
outputshapes an arbitrary result map (output_mapping: key → value). It’s generic plumbing — useful forGENERAL_PURPOSEworkflows or subworkflows that hand structured data back to a caller. It does not create servable media.media_outputdeclares the workflow’s media result. It maps the produced asset onto the well-known fields a Generation needs —media_type(required:IMAGE/VIDEO/CAROUSEL) plusimage_path/video_path/image_paths. On completion, brain’sGenerationMappingServicereads this node and writes those values onto theGenerationrow, turning the result into a stored, servable asset.
A media-generation workflow must end in a media_output node. Without one, the workflow exposes no image/video output — sirloin can’t derive a media contract for it, and it won’t work as a generation type. Reach for media_output for anything user-facing; use plain output only when you genuinely need a non-media data shape.
3. Drafts, publishing, and versions
A workflow is a draft while you edit it. Drafts are fully runnable from flank and fennec for testing, but they are invisible to external services (sirloin/brisket can’t see them).
Publishing freezes the current graph as a numbered, immutable version. External executors run the latest published version by workflow name. Editing after publish creates a new draft on top — the published version keeps serving until you publish again, and a republish (e.g. a pricing change) affects only new executions, never in-flight or past ones. Subworkflows follow the same model: a workflow pins a specific subworkflow version, so updating a subworkflow never silently changes already-published workflows.
Publishing also retires the previously published version of the same name to SUPERSEDED — full status semantics (one-published-per-name invariant, what superseded versions can still do, archive/delete rules) are documented in brain’s Lifecycle and versioning.
4. Run vs. test run (sandbox)
There are two ways to execute, and the difference matters:
- Run (execute) — runs the latest published version by name. This is the real thing: it produces exactly one
Generation(realmedia_id, persisted media, moderation enforced). This is the path sirloin uses for actual user generations. - Test run (sandbox) — runs a draft by id, at any publish status, without Generation wiring. It executes the graph so you can inspect the per-node trace, but by default it creates no Generation and persists no media (you can opt in with a “create generation” flag). Sandbox is how you iterate on a draft before publishing — fast feedback, no side effects, no charge.
Typical loop: build the draft → sandbox it until the graph and outputs look right → publish a version → it goes live for run.
5. Impact on fennec (and brisket)
The same workflow contract brain serves drives the admin and user surfaces, so what you build here shows up downstream:
- fennec (admin) renders the workflow’s input form from the contract, and — unlike brisket — can execute DRAFT workflows (the sandbox path) for pre-publish testing, supply internal-only inputs (input gallery, character reference picker), and inspect per-node execution logs. Moderation outcomes from the
media_output/inference path surface in fennec’s moderation dashboard. - brisket (user-facing) only ever sees published
MEDIA_GENERATIONworkflows. It renders the form and live pricing from the same contract; sirloin re-evaluates pricing and is the charge authority.
Practically: a flow isn’t “real” for users until it is MEDIA_GENERATION, ends in media_output, and has a published version — until then it lives only in the flank/fennec authoring-and-testing world.
6. Examples: presets on top of workflows
An example is a curated preset on top of a published MEDIA_GENERATION workflow: a display media (the hero image/video users browse on the explore page) plus the exact inputs that produced it. Selecting an example in brisket opens the workflow’s generation form pre-configured with those inputs, so the user starts from a known-good result instead of a blank form.
Coexistence note: workflow-backed examples currently live side by side with legacy examples (the pre-T-Bone kind with a single free-text prompt and client-side pricing). The same brain endpoint returns both; brisket branches per kind. Legacy examples keep working unchanged until the retroactive migration (FOXY-427) retires that path.
How to create one (promotion-based — you never author an example from scratch):
- Generate the source media. Run the workflow (in fennec’s Media Generator, or as a real user generation) until you have a result worth showcasing. The example will carry a copy of the inputs that execution actually ran with, so the source generation is the authoring step.
- Promote it in fennec’s Generation Gallery. Find the generation and use Add to examples. The promote flow copies the workflow name (+ version, kept as provenance only), the trigger inputs, and the display media, and lets you set the example type (basic / signature / video) and tags. Explicitness is inherited from the source workflow’s variant — an SFW workflow cannot produce an NSFW example.
- Choose the anchoring — workflow or family. If the source workflow is a member of a generation family, the promote flow offers a choice:
- Pin to this workflow — the example always runs exactly this workflow. The FE form renders this workflow’s contract, no axis pickers.
- Expose the family — the example carries the
family_key. The FE form additionally renders the family’s axis pickers (e.g. model, 18+), defaulted to the source variant’s tuple; the user can toggle to sibling variants, and the resolved variant drives the field definitions, pricing, and dispatch.
- Configure per-input visibility (optional). Each workflow has a Workflow Example Config (fennec → workflow example config page) with per-input
visible/editableflags. This config is per workflow, not per example — for a family-backed example, the entry matching the variant the user’s axes resolve to governs the form. A workflow with no config saved is fully visible + editable.
What the flags do on the brisket form:
visible: false— the field is removed from the rendered form entirely. Its authored value is treated as IP: sirloin strips it from the example payload before it reaches the browser and re-injects it server-side at generate time (keyed byexample_id), so the value never round-trips through the client.editable: false— the field renders locked (disabled) with the authored value prefilled; user edits are ignored and the server enforces the authored value at dispatch.- Hidden or locked inputs are excluded from the required-field gate — the server owns their values, so they never block the generate button.
- Everything else renders as a normal contract-driven field, prefilled with the example’s authored value where one exists.
Because examples resolve latest-published by workflow name (the stored version is provenance, not a pin), a republish changes what an example runs — the additive-only contract compatibility check at publish time is what keeps existing examples from breaking.
Primary Source Paths
apps/flank/app/routes/— editor and execution routes (e.g.workflows.$id.tsx,executions.$id.tsx)apps/flank/app/components/flow/— the ReactFlow editor (FlowEditor, NodePalette, NodeConfigPanel, nodes)apps/flank/app/lib/— client utilities and the brain HTTP client / server functions
Related Flows
Related Standards
Local Commands
cd apps/flank && pnpm typecheckcd apps/flank && pnpm lintcd apps/flank && pnpm testcd apps/flank && pnpm dev(Vinxi dev server on port 3100)