Blueprints

A blueprint is a scene written down. One JSON document carries everything that makes a scene a scene: identity, assets, recurring jobs, evaluable tasks, who has access. It does not have to carry the content. A note can sit beside it as a markdown file, a view as an HTML file, a script as TypeScript, each referenced with $file, and that is how nearly every blueprint we ship is written. Apply it and the scene appears. Export a live scene and you get the same shape back, ready to fork.

Blueprints are the form behind the catalog at /blueprints and the older form of provider scenes. This page is about the document: what is in it, how it splits across files, and what it is not.

What's in a blueprint

{
  "version": 1,

  "id":   "scn_my_world",
  "slug": "my-world",
  "name": "My World",
  "icon": "globe",
  "tint": "#4C8BF5",

  "settings": {
    "description": "Built from a blueprint."
  },

  "assets":       [/* scene-owned content, inline or { "$file": "./notes/prep.md" } */],
  "linkedAssets": [/* foreign rows pinned in place */],
  "schedules":    [/* recurring agent tasks */],
  "tasks":        [/* evaluable scenarios */],
  "members":      [{ "email": "you@example.com", "role": "owner" }],
  "secrets":      {/* credential refs: "brave/account:brave1": { "from": "kern:BRAVE_ACCT" } */},
  "scenes":       [/* child scenes, recursive */]
}

Two things are worth understanding deeply.

Identity sits at the top, settings sit in settings. Identity (id, slug, name, icon, tint) is load-bearing for cross-references and the visual header. Configurable state (description, feature flags, welcome suggestions, notification preferences) lives in settings, same as the iOS settings sheet. The applier only writes columns the blueprint declares, so anything you set in iOS that the blueprint doesn't carry stays intact.

assets[] versus linkedAssets[] is the load-bearing split. Scene-owned content goes in assets[], where the blueprint creates these rows and owns their contents. Foreign rows go in linkedAssets[]: account chips, GitHub repos, Slack channels, SAP entities. Their DB ids stay stable across applies; the blueprint pins them, can optionally edit their content in place, but never forks them into duplicate rows.

In practice it is a file and a folder beside it

Anywhere a field holds a long string, write { "$file": "./path" } and keep the string in a real file. The blueprint we watch our own production with is a small JSON document plus two markdown files:

server-health.blueprint.json
server-health/
├── runbook.md      # the endpoint table and the rules
└── health-log.md   # what the daily probe appends to

The document points at them:

{
  "version": 1,
  "id":   "scn_ops_health",
  "slug": "server-health",
  "name": "Server Health",

  "assets": [
    { "id": "runbook", "type": "daslab/note", "name": "Runbook",
      "fields": { "content": { "$file": "./server-health/runbook.md" } } },

    { "id": "health_log", "type": "daslab/note", "name": "Health log",
      "fields": { "content": { "$file": "./server-health/health-log.md" } } }
  ],

  "schedules": [
    { "id": "daily-health", "cron": "0 7 * * *", "title": "Daily server health probe",
      "prompt": "Read the Runbook, probe each endpoint, append a dated entry to the Health log." }
  ]
}

The runbook is markdown, so you edit it in your editor, review it in a pull request, and diff it line by line. A view stays HTML with its highlighting; a script cell stays TypeScript. Only the structure stays in JSON, which is the part JSON is good at. export --extract-html writes this shape for you: one file per large field, a $file ref where the string used to be. Apply reads them back in before anything reaches the database, so a blueprint with $file refs and the same blueprint with everything pasted inline produce the identical scene.

Take that to its end, move identity into scene.json and let every asset be its own file, and no document is left, only the folder. That layout is on Export and import. Both forms go through the same importer, and the same command reads either.

Apply is the importer with a document

daslab scene push   ./my-world.blueprint.json   # the document, with the files it references beside it
daslab scene push   ./my-world/                 # the same scene as a folder
daslab scene export <sceneId> --json --children --extract-html --out=./my-world.blueprint.json

daslab blueprint apply and daslab blueprint push are the same command under older names.

Apply is idempotent: same blueprint, same input, same output. Deterministic IDs from (sceneId, type, externalId) mean re-running upserts the same rows. Merge, conflicts, --strict, the commit each apply leaves, and how credentials travel as kern: and env: references are the importer's rules, and they are the same for a document as for a folder: Export and import states them once.

What a blueprint is not

  • Not a configuration management tool. It mutates the database, but only for the scene tree it describes. It doesn't manage server state, user accounts outside the scene, or anything cross-scene.
  • Not a build artifact. The scene folder is the source; the JSON document is its compiled form. The applier writes to the DB from either, and what it wrote is exactly what the folder says.
  • Not a backup format. It captures the structure of a scene plus its scene-owned content. Job histories, conversation traces, scene annotations, and runtime state are not included; those live in their own systems with their own retention rules.
  • Not a code substitute. Blueprints declare; they don't compute. Schedules and tasks reference prompts the agent reads at runtime, but the format itself has no functions, loops, or conditionals.

Where blueprints come from

We ship a small set of high-quality starting points at /blueprints. Anything you build in the iOS app is exportable as a blueprint. Anything someone else built is forkable as a blueprint.

Reference

The full v1 spec, every field and every semantic: scenecast/docs/scene-blueprint.md.

What's next

Updated 2026-08-25