Briefs
Every Studio project starts from exactly one route:
POST /api/studio/projects { title?, brief, generatorId?, inputs? } → Projectbrief is the only required field. There are two shapes this call takes in practice:
- A free-text brief, typed into the Studio’s hero composer, the omnibar, or curled directly:
“Write a 1200-word blog post about why small teams ship faster than big ones.” No
generatorId. - A generator run:
generatorIdplusinputsmatching that generator’s JSON Schema form (see Generators). The title defaults to"<Generator name>: <first line of the brief>".
Either way, the route inserts the Project row (status: 'planning'), writes one plan-kind trace
(“Project received. Planning.”), and — critically — returns immediately. Planning, staffing and
execution all happen after the response, in the background (onProjectCreated in
apps/api/src/modules/studio/run.ts, fired via setImmediate so the HTTP request never waits on
it).
What the planner does
plan() (packages/agents/src/planner.ts) takes exactly one of two paths:
- With a generator — the generator’s own
steps[]are instantiated deterministically: no model call at all. Each step’sinstruction/titletemplate ({{inputs.x}},{{steps.id.output}}) is rendered against your actual form values, right away. - Without a generator — a single
strong-tier structured call decomposes the brief into 3–9 tasks: a key, title, instruction, role, skills, tier, dependencies and output kind for each. The system prompt is explicit about the shape it wants: short slugs for keys, roles from a fixed set (researcher, writer, editor, engineer, designer, analyst, marketer, lead),cheapfor drafting/summaries,standardfor code/reasoning,strongreserved for genuinely hard tasks (the planner itself is the onestrongcall in the whole pipeline other than QA), and — always — the first task is research.
The graph is always repaired, never trusted blindly
Whichever path produced the tasks, the planner runs the same safety net over the result
(repairGraph() in packages/agents/src/graph.ts):
- Duplicate task keys are de-duplicated.
- Dependency cycles are broken.
- If nothing in the plan is a research task, one is inserted at the front automatically — with
real
web.research/web.search/web.fetchskills — and every root task is made to depend on it. This runs whether or not the brief looks “factual”; the planner’s own system prompt asks for research first, but the repair step enforces it structurally too, so a model that forgets never ships a project built on invented facts. - If the model returns fewer than 3 tasks, a “polish” task is appended so no plan ships as a single giant step.
- More than 9 tasks are truncated to 9, with a note explaining the truncation.
Every one of these interventions is logged as a human-readable note, visible in the project’s trace, not a silent correction — you can always see exactly where the planner’s raw output was adjusted and why.
A concrete plan
For the blog post brief above, a typical plan looks like:
{ "title": "Why small teams ship faster than big ones", "tasks": [ { "key": "research", "role": "researcher", "skills": ["web.research", "web.search", "files.write"], "tier": "cheap" }, { "key": "outline", "role": "strategist", "dependsOn": ["research"], "tier": "cheap" }, { "key": "draft", "role": "writer", "dependsOn": ["outline"], "tier": "cheap" }, { "key": "polish", "role": "editor", "dependsOn": ["draft"], "tier": "cheap", "outputKind": "markdown" } ]}This is, in fact, exactly the shape of the seeded Blog post generator — free-text briefs and generators converge on the same kind of task graph; a generator just guarantees that exact shape every time instead of leaving it to the model.
What happens next
Once the graph exists, Projects covers team assembly and execution, and Outputs covers what you get back and how it’s reviewed.