--- title: 'Project manifests' description: 'How a Stackbone project is declared: workspace discovery by convention (deep-agents//index.ts + workflows/), the optional agent.yaml manifest, and the optional stackbone.config.ts override.' position: 26 --- # Project manifests > A Stackbone project is a **workspace**: a set of agents plus durable > workflows. The CLI discovers the workspace **by convention**: it scans the > files on disk, so most projects need no extra manifest. An agent is any > folder under `deep-agents/` with an `index.ts` entry; a > workflow is any `workflows/.workflow.ts`. > > The workspace may also carry a single, **optional** `agent.yaml` at its > root. It does one job: it moves the database schema and migrations paths off > their defaults. Most workspaces need none of it. > > `stackbone.config.ts` is a **separate, also optional** override. It wins > over the convention scan for workflows, it can restate the deep-agent > registry under `deepAgents` (to hide one from Studio with > `$internal: true`), and it carries `build.external`, which nothing on > disk can imply. See > [Configuration](/docs/cli/reference/configuration#per-project). > > `stackbone init` writes **neither** file. A fresh workspace runs on the > conventions alone, and you add either file by hand when you need it. When > `agent.yaml` is present, the CLI validates it in strict mode; both files > belong under version control. ## Workspace discovery by convention The runtime derives the workspace from the files on disk, so there is no hand-maintained registry. `stackbone dev` reads the same two conventions the runtime does: - **Agents**: every folder under `deep-agents/` that contains an `index.ts` entry file. The folder name is both the agent's identity and the `model` a client selects to talk to it. There is no dependency marker to declare: the folder plus the entry file makes it an agent. - **Workflows**: every `workflows/.workflow.ts`. The workflow name is the file basename without the `.workflow.ts` suffix, and the exported function is the camel-cased name plus `Workflow` (e.g. `workflows/qualify-lead.workflow.ts` exports `qualifyLeadWorkflow`). You add pieces with `stackbone add` (`stackbone add agent `, `stackbone add workflow `, `stackbone add workflow-agent `), which writes the files in these locations so the next `stackbone dev` picks them up, with nothing else to wire. ### An agent directory An agent is a folder under `deep-agents/` with an `index.ts` that default-exports an agent definition built with `defineDeepAgent` from `@stackbone/sdk/deep`: ```ts // deep-agents/support/index.ts import { defineDeepAgent } from '@stackbone/sdk/deep'; export default defineDeepAgent({ name: 'support', model: 'anthropic/claude-haiku-4.5', }); ``` `name` must match the folder. The agent's instruction is not in the file: you write it in Studio, in the [prompt catalogue](/docs/sdk/platform/prompts), under the prompt keyed by that same name. An agent carries no per-piece manifest and no `package.json` of its own: its runtime dependencies live in the workspace root `package.json`, so the runtime resolves one copy of each. See [Agents](/docs/sdk/agents/overview) for how an agent runs and how tools reach the ambient `stackbone` client. ### A workflow file A workflow lives at `workflows/.workflow.ts` and exports a durable async function marked `'use workflow'` (named `Workflow`) that runs on top of the durable [Workflow SDK](https://workflow-sdk.dev/docs). Each `'use step'` inside runs once; the runtime persists it and replays it from its event log on retry. See [Workflows](/docs/sdk/workflows/overview) for the full model. A workflow declares its input/output **contract** by convention too. Export `inputSchema` and `outputSchema` (a [Zod](https://zod.dev), or any Standard-Schema, object) alongside the workflow function, or put them in a `.contract.ts` sibling. The build harvests those sibling exports: ```ts // workflows/onboarding.workflow.ts import { z } from '@stackbone/sdk'; export const inputSchema = z.object({ email: z.string().email(), plan: z.string() }); export const outputSchema = z.object({ welcomed: z.boolean() }); export async function onboardingWorkflow(input: z.infer) { 'use workflow'; // ... return { welcomed: true }; } ``` These sibling schemas drive validation on the workflow's start endpoint and the schema the CLI shows for `stackbone workflows schema`. ### Editor types `stackbone dev` writes While `stackbone dev` runs, it generates a few `.d.ts` files under `.stackbone/` from the discovered workspace (your `deep-agents/` folders and `workflows/` files, plus your connectors + config schema) so the ambient client is typed in your editor. They are derived artifacts, git-ignore them: | File | Types | | --------------------------- | ----------------------------------------------------------------- | | `.stackbone/agents.d.ts` | `callDeepAgent('', ...)` narrowed to your agent names. | | `.stackbone/workflows.d.ts` | `stackbone.workflows.start('', ...)` narrowed the same way. | | `.stackbone/connect.d.ts` | `stackbone.connection('')` and its connector methods. | | `.stackbone/config.d.ts` | `stackbone.config.get('')` typed from your config schema. | ## `agent.yaml`: the optional workspace manifest A workspace may carry a single `agent.yaml` at its root describing project-wide runtime, database, connections and protocol settings. Most workspaces don't need one, because the CLI falls back to defaults for everything it would configure. Its schema is locked under `apiVersion: stackbone.ai/v1`. When the file is present, the CLI validates it in **strict mode**: any unknown top-level or nested key fails the parse with an error that names the offending key. Typo `runtim:` or add a custom key and the next `stackbone ` exits non-zero. Read the block reference below with one caveat: the schema accepts more than the CLI acts on. Only `database` and `dev` change behaviour today. Every other block parses and takes its default, and each section says so. ### Minimum manifest ```yaml apiVersion: stackbone.ai/v1 name: my-workspace ``` When you omit a block, the CLI fills the defaults documented below. It keeps path strings verbatim and resolves them at command execution time relative to the project root, never at parse time. ### `apiVersion` (required) Locked to `stackbone.ai/v1`. Bumped only on a breaking change. ### `name` (required) Human-readable workspace name. Must be a non-empty string. ### `version` (optional) A display-only label (e.g. `v0.2.0`). The CLI does not validate it as semver or check it for uniqueness, and nothing reads it today. It is a note for you and your team. The version that identifies what is running is the image tag you register with `stackbone link --tag`, see [Commands → `stackbone link`](/docs/cli/reference/link). ```yaml version: v0.2.0 ``` ### `runtime` (optional) ```yaml runtime: engine: node # only supported value today entry: src/index.ts # default ``` A leftover from the single-file agent shape. A workspace boots from the pieces the convention scan finds, so nothing reads `engine` or `entry` today. The block still parses (`engine` must be `node`; `bun` is reserved and rejected), and the four reserved builder keys below still fail the parse. Leave the block out. ### `database` (optional) ```yaml database: schema: ./src/schema.ts # default migrations: ./.stackbone/migrations # default ``` Where the CLI looks for your [Drizzle](https://orm.drizzle.team/) schema and the SQL files `stackbone db migrate create` writes. A workspace's agents and workflows all talk to the one install Postgres, so one schema covers the whole workspace instead of one per agent. Override either path when your layout differs: ```yaml database: schema: db/schema/index.ts migrations: ../shared/migrations/ ``` The CLI keeps both values verbatim and resolves them against the project root at command time, so relative paths work regardless of where you ran `stackbone` from. Three commands read this block: `stackbone dev` (pre-boot migrate and the schema watcher), `stackbone db migrate ...`, and `stackbone build`, which carries the migrations folder into the bundle. ### `dev` (optional) ```yaml dev: autoMigrate: true # default; set false to opt out ``` `stackbone dev` always applies pending migrations before booting and applies any new `.sql` you drop into the migrations folder while it runs. `dev.autoMigrate` only controls what happens when you edit `src/schema.ts` mid-session: with the default `true` the CLI generates a migration from the diff, applies it, and restarts; with `false` it prints a hint to run `stackbone db migrate create ` yourself. ### `rag` (optional) ```yaml rag: embeddingModel: openai/text-embedding-3-small # default ``` Names the embedding model for the RAG pipeline. Accepted, but **not wired**: nothing forwards this value out of the manifest today, so `stackbone.rag` falls back to `openai/text-embedding-3-small` whatever you write here. Pass `model` on the call instead, and pick it **before** ingesting the first document: the pipeline pins each collection to the dimensionality of the model that created it. See [`stackbone.rag`](/docs/sdk/data/rag). Unknown keys fail the parse. ### `protocol` (optional) ```yaml protocol: required: 10 ``` A creator-side floor on the Stackbone Agent Protocol contract version. The SDK supports such a floor: below it, every gated `stackbone.*` call fails closed with `contract_version_unsupported` before any per-module capability check runs (the chat and workflow routes your workspace serves are **not** capabilities). Accepted, but **not wired**: nothing forwards this value out of the manifest today, so the SDK's built-in floor applies whatever you write here. Unknown keys fail the parse. To check a target against the floor by hand, run [`stackbone contract validate`](/docs/cli/reference/contract#stackbone-contract-validate). ## Accepted but not wired yet: `connections` and `automations` Two more blocks parse without error and change nothing today. They are in the schema so a manifest you write now stays valid once the install flow reads them. Leaving them out costs you nothing. ```yaml connections: required: [stub-mail, acme-crm] # connector accounts this workspace expects automations: recipes: [] # connector-triggered pipelines to seed at install time ``` `connections.required` is the list of [Stackbone Connect](/docs/home/features/integrations) connectors your code calls with `stackbone.connection('')`. Connecting those accounts is an operator action in Studio today, whether or not you declare them here. The CLI checks each id for format only. Both blocks default to empty, and unknown keys inside them fail the parse. ## Reserved `runtime.*` fields The schema recognises four builder keys and rejects each one today with a hand-written message explaining why. They are reserved so future support can land without changing the manifest shape. ### `runtime.customDockerfile` (reserved) ```yaml runtime: customDockerfile: ./Dockerfile # ✗ rejected ``` The Stackbone builder owns the image: it renders a Dockerfile from a template against your `package.json` and lockfile. You cannot bring your own today. ### `runtime.systemDeps` (reserved) ```yaml runtime: systemDeps: ['libvips', 'poppler-utils'] # ✗ rejected ``` The builder runs Node-only on a fixed base image. The field stays reserved until someone needs it, and it will then take a whitelist of allowed packages instead of a free-form array. ### `runtime.buildSecrets` (reserved) ```yaml runtime: buildSecrets: ['NPM_TOKEN'] # ✗ rejected ``` Build-time secret storage is not built yet. Runtime secrets, the values your published agents read from their environment, keep working through the existing secrets surface; this entry covers _build-time_ secrets only. ### `runtime.packageManager` (reserved) ```yaml runtime: packageManager: yarn # ✗ rejected ``` The builder detects pnpm / npm / yarn from the lockfile in the project root (`pnpm-lock.yaml`, `package-lock.json`, or `yarn.lock`). You do not need an explicit override today, so the field is reserved. ## Which manifest each command reads The CLI discovers the workspace by convention: every `deep-agents//index.ts` on disk, plus every `workflows/.workflow.ts`. An optional `stackbone.config.ts` overrides that registry. The optional root `agent.yaml` supplies the database paths when it is present. | Command | What it reads | | ----------------------------- | -------------------------------------------------------------------------------- | | `stackbone dev` | The discovered workspace, plus `agent.yaml` `database` + `dev`, when present. | | `stackbone db migrate ...` | `agent.yaml` `database`, when present. | | `stackbone build` | The discovered workspace, plus `agent.yaml` `database.migrations`, when present. | | `stackbone contract validate` | `agent.yaml` itself. The verb exits `3` when the file is absent. | Every file here is yours to edit and belongs under version control. The control plane never writes to them. > This page is the reference for the manifest. A coding agent can read it > directly: `stackbone docs` prints how to connect one to this site over MCP.