Brain
Brain
Responsibility
Brain is the NestJS service for generation records, character metadata, provider integrations, and Prisma-owned data in the fennec schema. It also provides reference image nudification (synthetic NSFW variant generation from SFW images) and a body-parts reference library used during character creation.
Under T-Bone, brain owns the workflow engine — the content framework that replaces hand-coded GenerationType branches: workflows, subworkflows, nodes, executions, contracts, and the per-provider inference adapters all live here and are served over an HTTP API (see Workflow engine below).
Runtime
Brain runs as a NestJS HTTP service. Sirloin consumes its OpenAPI-generated HTTP client, Brain calls Round over gRPC for model-serving work, and completed workflows can notify Sirloin over HTTP.
Primary Source Paths
- apps/brain/src/modules/
- apps/brain/src/modules/domain/workflow/ (T-Bone workflow engine: engine, nodes, contracts, executions)
- apps/brain/src/generated/
- apps/brain/prisma/schema.prisma
Contracts And Generated References
- Brain HTTP bootstrap and OpenAPI setup live in apps/brain/src/main.ts.
- Round generated client code lives under apps/brain/src/generated/round/.
- Sirloin consumes Brain through apps/sirloin/pkg/brain-client/.
Workflow engine
The T-Bone workflow engine lives under apps/brain/src/modules/domain/workflow/. It is the content framework that turns one-off, hand-coded generation types into framework-driven workflows. Core vocabulary:
- Workflow — a named, versioned graph (stored as JSON) describing end-to-end how one piece of media is generated, from raw inputs through provider inference to a stored result. Declares an input contract and an output contract, carries embedded pricing rules, and normally maps media execution to one
Generationrow. Sandbox execution creates no Generation unless explicitly requested and applicable. Workflows are drafts until published, which freezes a numbered version. Identified byname+version. See Lifecycle and versioning. - Subworkflow — a reusable graph fragment that runs only inside a workflow (one-level nesting, enforced by the save validator). A workflow pins a specific subworkflow version. Each subworkflow node persists its inline run as a child execution linked back to the parent and the node that spawned it. Siblings in a parallel group claim their child concurrently and without a lock: a partial unique index (
idx_workflow_execution_one_running_child_per_parent_node) admits at most one RUNNING attempt per parent node, so a losing claimant reuses the winner’s attempt instead of starting a second run. Only a missing or CANCELLED attempt starts fresh; RUNNING and terminal attempts are authoritative and get resumed or replayed. - Node — the atomic step: a backend method wrapped by a base class that records start/end events, timing, inputs, outputs, and errors so each step is independently inspectable. Nodes live inside a workflow’s graph and are versioned with it.
- Engine — the runtime that interprets the graph. It auto-parallelizes independent nodes by topological depth, persists a per-node execution trace (for profiling and crash resume), and enforces cross-cutting behavior an author can’t skip (the moderation wrap on inference nodes). On boot it re-enqueues executions left RUNNING by a crash.
- Execution — a single run: RUNNING → COMPLETED, FAILED, or CANCELLED, with a per-node trace. The pinned graph is captured at start so a mid-flight edit can’t corrupt an in-progress run. CANCELLED is recorded by the executor on abort-signal propagation (supersession, parent deadline abort into a nested run); whole-run budget expiry fails the execution. HTTP API-triggered cancellation of in-flight executions is out of scope for v1.
Config templates and expressions
A node’s config values reference the workflow’s inputs and upstream node outputs
through {{...}} placeholders, resolved at execution time. Two dialects
coexist: the original path-and-fallback parser, which is feature-frozen, and the
{{= ...}} expression dialect that adds arithmetic and rounding. See
Workflow Template Expressions.
Lifecycle and versioning
Status lifecycle: DRAFT → PUBLISHED → SUPERSEDED (replaced by a newer publish) → ARCHIVED (per-name, explicit). Publishing atomically retires the previously published version of the same name to SUPERSEDED, so exactly one version per name is PUBLISHED at any time — enforced by a partial unique index (idx_workflow_one_published_per_name). A superseded version is out of the external-executor path (external executors always resolve the latest published version by name), but it is not dead: subworkflow pins that point at it keep executing it, and it can still be sandbox-run by id (brain logs a warning so testing an outdated version is a deliberate act). Only drafts are deletable; published or superseded versions must be archived first, and archived versions are retained — they keep the name reserved and preserve history.
Contract and surfaces
A workflow’s contract (input_schema, pricing, output_schema) is HTTP-served and is the single source of truth that the UI surfaces render from and that sirloin re-evaluates pricing against:
- brisket — user-facing forms, dynamic pricing, result presentation.
- fennec — admin: the same forms plus executing DRAFT workflows, internal-only inputs, and per-node logs.
- flank — the visual workflow editor, re-pointed to read/write brain over HTTP.
- Foxy360 / AgentOS — the agent tool surface forwards the caller’s Clerk token to the same Brain endpoints.
Two HTTP surfaces: the “Beef” surface (@Controller('api/*'), API-key auth) is used by sirloin for service-to-service calls; the Clerk-authenticated /workflow/* controllers serve the human surfaces. Authoring, drafts, sandbox execution, and administrative lists default to ADMIN. Published discovery, contracts, execution, and polling permit CREATOR and above on the explicitly decorated endpoints.
HTTP surfaces and auth
Ordinary sirloin service-to-service calls use the api module (Beef surface)
with an API key. The explicit exception is sirloin’s Foxy360 / AgentOS tool
surface: those tools call Clerk-authenticated workflow endpoints and forward the
caller’s token rather than impersonating an admin. Do not point other sirloin
backend flows at Clerk-authenticated controllers.
Contract changes and generated clients
The OpenAPI spec served at /api-json is the source for two committed, generated clients: the Go client sirloin consumes (apps/sirloin/pkg/brain-client/, regenerated with make generate-brain) and the fennec zod client (apps/fennec/src/lib/backend/generated/brain/, regenerated with make generate-brain-fennec-client). They do not regenerate automatically. After any Swagger-visible controller or DTO change, restart Brain so the live spec refreshes, regenerate both clients from the repo root, inspect the generated diffs, and validate affected consumers. See apps/brain/AGENTS.md for the sequence.
Inference, moderation, pricing
- Adapters — one generic integration per inference provider (FAL, WaveSpeed, RunPod, OpenRouter, Hive); the model is an input on the inference node, not its own adapter. A model that breaks the per-provider assumption can get a model-specific adapter.
- Moderation wrap — Hive visual moderation on an inference node’s inputs and outputs, enforced by the engine; a workflow disables it only via an explicit flag when its semantics require it (e.g.
is_nsfw = true). - Pricing — rules are embedded in the contract, but the charge authority stays in sirloin; brisket shows the live price, sirloin re-evaluates server-side before deducting credits.
- Generation mapping — normal media executions produce a
Generationwithgeneration_type = WORKFLOW,workflow_name, andworkflow_version, keyed by themedia_idin the trigger inputs. Sandbox runs omit it unlesscreate_generationis explicitly requested and supported.
Execution timeouts
The executor layers three budgets. Every wrapper is clamped to the remaining whole-run deadline so a single node cannot outlive the execution.
| Budget | Default | Notes |
|---|---|---|
Whole-run (totalTimeoutMs) | 60 min | Hard ceiling for the execution (apps/brain/src/modules/domain/workflow/engine/executor.service.ts:144-145). |
Ordinary per-node (nodeTimeoutMs) | 5 min | Default executor wrapper for nodes that do not declare their own budget. |
call_model poll / wrapper | 10 min | Owned by the node’s timeout_ms / max_poll_ms inputs. Unset max_poll_ms follows timeout_ms. Shared helpers live in apps/brain/src/modules/domain/workflow/nodes/gen/call-model-timeouts.ts:1-16. |
Long-running nodes opt in via BaseNode.executionTimeoutMs:
call_model— returns its own poll budget so the ordinary 5 min node ceiling cannot preempt a documented 10 min (or author-raised) inference wait.subworkflow— uses the parent’s remaining deadline and passestotalTimeoutMs+signalinto the nestedExecutorService.execute, so child inference and persistence cancel when the parent budget ends (not a fresh 60 min child window).
Not in scope for v1: HTTP API-triggered cancellation of in-flight executions (engine deadline enforcement and abort-signal propagation, including into subworkflows, are supported), a first-class Templates concept, and brain-owned secrets management.
Module layering and DTO conventions
These are standing architectural decisions; follow them when adding or reviewing code.
- Domain services are DTO-unaware. Services under
modules/domain/*/services/operate on domain interfaces (e.g.IExample) and throw transport-agnostic domain exceptions (see Brain error handling). They never import request/response DTO classes. - Mapping happens at the edge. Controllers translate request DTOs into domain-shaped inputs inline and build responses via static, synchronous
Response.toDto(entity)methods on the DTO class. Canonical example:apps/brain/src/modules/domain/generation/controllers/examples.http.controller.ts. - The
apimodule is a presentation layer, and is a deliberate deviation.modules/domain/api/owns the Beef (sirloin-facing, API-key) surface. When building a wire DTO requires async cross-module reads — something a statictoDtocannot do — the mapping lives in an injectable presenter/assembler service inside theapimodule (e.g.BeefExampleService, which decorates examples with their workflow contract and family variant, memoizing lookups per request). This keeps domain services DTO-unaware and controllers thin; the DTO-awareness is confined to the layer whose only job is producing the wire format. Do not replicate this pattern inside domain modules — if a domain module needs composition, compose domain objects there and map in the controller orapilayer. - Cross-module access goes through exported services, not repositories. A module reaches into another module via that module’s service layer (e.g.
WorkflowService.getLatestPublished), never by injecting the other module’s repository directly. Repositories are module-private so caching, validation, and access rules stay in one place.
Workflow examples: input visibility/editability
Per-input visible/editable flags for workflow-backed examples are not
stored on the example. They live in WorkflowExampleConfig (one row per
workflow, edited in fennec’s Workflow Example Config page) and are resolved
live when brain serves examples to sirloin: BeefExampleService ships a
top-level workflows: { [name]: { contract, input_overrides } } dictionary
covering each example’s own workflow plus all of its family’s member
workflows. Consequences:
- Config edits apply retroactively to every example of that workflow, including sirloin’s hidden-value strip (retroactive IP protection) and authored-value injection at dispatch.
- Family-backed examples get per-variant flags: the frontend picks the entry for the workflow actually resolved by the user’s axis selection.
- A missing config means all inputs are visible and editable.
- There is no per-example snapshot: the former
generation_example.input_overridescolumn was dropped before the feature reached production, and the config is the single source of truth.
Related Flows
- Media Generation
- Character Creation (includes reference image nudification)
- Brain API — Body Parts Library & Reference Nudify
- Brain Inference Providers (provider inventory, fallback chains, traffic split)
Related Standards
Related Decisions
Generation and provider decisions live under docs/src/content/docs/decisions/ as they are migrated.
Operations
Brain uses BullMQ/Redis for async generation workflows and PostgreSQL via Prisma for the fennec schema.
Media generation paths that handle provider videos and source media use disk-backed temporary files instead of whole-file buffers where practical. The storage module owns temporary directory cleanup and bounded file I/O through TempFileService and FileIoLimiterService; video persistence, provider preupload, reference-video trimming, and first-frame extraction use path/stream APIs in apps/brain/src/modules/application/storage/services/ and apps/brain/src/modules/application/video/services/.
File I/O limiter state is exported as OpenTelemetry gauges for operations checks; see Observability.
Local Commands
- cd apps/brain && pnpm lint
- cd apps/brain && pnpm tsc
- cd apps/brain && pnpm test
- cd apps/brain && pnpm start:dev