Skip to content

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/flank or fold into fennec) is an open decision. Today it lives in apps/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 over FlankExecutionService — has been fully removed. The proto/flank/v1 package, 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. Like GENERAL_PURPOSE it 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:

  • output shapes an arbitrary result map (output_mapping: key → value). It’s generic plumbing — useful for GENERAL_PURPOSE workflows or subworkflows that hand structured data back to a caller. It does not create servable media.
  • media_output declares the workflow’s media result. It maps the produced asset onto the well-known fields a Generation needs — media_type (required: IMAGE / VIDEO / CAROUSEL) plus image_path / video_path / image_paths. On completion, brain’s GenerationMappingService reads this node and writes those values onto the Generation row, 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 (real media_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_GENERATION workflows. 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):

  1. 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.
  2. 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.
  3. 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.
  4. Configure per-input visibility (optional). Each workflow has a Workflow Example Config (fennec → workflow example config page) with per-input visible / editable flags. 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 by example_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

Local Commands

  • cd apps/flank && pnpm typecheck
  • cd apps/flank && pnpm lint
  • cd apps/flank && pnpm test
  • cd apps/flank && pnpm dev (Vinxi dev server on port 3100)