Skip to main content

Bundle namespaces

An app isn't just one YAML file - it's a bundle directory: the YAML plus a structured set of supporting files (prompts, skills, behavior profiles, assets, and YAML fragments). Six template namespaces let YAML reference these files; the compiler resolves them inline at compile time.

Bundle layout​

text
my-app/
├── app.yaml
├── prompts/
│ ├── system.md # {{prompt.system}}
│ ├── coordinator.md # {{prompt.coordinator}}
│ └── system.fr.md # locale-suffixed variant (see below)
├── skills/
│ ├── commit.md # {{skill.commit}}
│ └── review.md # {{skill.review}}
├── behavior/
│ └── strict_dev.yaml # {{behavior.strict_dev}}
├── assets/
│ ├── logo.svg # {{asset.logo.svg}}
│ ├── icon.png # {{asset_b64.icon}} (small files only)
│ └── docs/
│ └── intro.md # markdown images auto-rewrite
├── fragments/
│ ├── main_brain.yaml # {{include:fragments/main_brain.yaml}}
│ └── shared_modules.yaml
├── widgets/ # not auto-loaded; {{include:widgets/X.yaml}}
│ └── stat_card.yaml
└── agents/ # auto-loaded, no include: needed
└── reviewer.yaml

The compiler walks the YAML, finds every {{namespace.X}} / {{include:path}} placeholder, and replaces it with the file content (or URL) BEFORE the compiler validates.

The 6 namespaces​

PatternFolderResolves to
{{prompt.X}}prompts/X.mdFile content (raw markdown)
{{skill.X}}skills/X.mdFile content
{{behavior.X}}behavior/X.yamlParsed YAML, returned as a JSON string
{{asset.X}}assets/XURL: a stable asset URL for that app
{{asset_b64.X}}assets/Xdata:<mime>;base64,<payload> URI
{{include:path}}<bundle>/pathParsed YAML fragment inlined into the parent structure

File extension fallback chain​

For prompt / skill namespaces, each of these extensions is tried in order; first match wins:

  1. .md
  2. .markdown
  3. .txt
  4. .prompt
  5. bare name (no extension)

So {{prompt.system}} finds prompts/system.md, prompts/system.markdown, prompts/system.txt, prompts/system.prompt, or prompts/system (in that order). If the key already has an extension ({{prompt.system.txt}}), it's tried verbatim first.

Locale variants​

When the DIGITORN_LOCALE environment variable is set on the machine that compiles the app, locale-suffixed variants win over the default:

text
prompts/
├── system.md # default
├── system.fr.md # used when DIGITORN_LOCALE=fr
└── system.es.md # used when DIGITORN_LOCALE=es

{{prompt.system}} with DIGITORN_LOCALE=fr resolves to prompts/system.fr.md if present, falls back to prompts/system.md otherwise. Useful for multilingual apps shipped with the same YAML.

YAML frontmatter on prompt files​

Standard markdown convention - when a prompt or skill file starts with a --- block, it's parsed as YAML metadata and stripped from the inlined content:

markdown
---
description: "Main system prompt for the assistant"
variables_required: [user_name, company]
max_tokens_estimate: 1200
---

You are an assistant...

The body (You are an assistant...) is what gets inlined into the YAML at the {{prompt.X}} callsite. Two fields are actually enforced at compile time: every name in variables_required must resolve to a known namespace or a declared dev.variables key, or compiling fails; max_tokens_estimate over 200 000 raises a warning. description, variables_optional, tags, locale, and author are recognised and carried along but not otherwise checked.

{{prompt.X}} - system prompts​

The most common use case: factor an agent's system prompt into a separate markdown file.

yaml
agents:
- id: assistant
brain: { ... }
system_prompt: "{{prompt.assistant_system}}"

prompts/assistant_system.md:

markdown
---
version: 1
description: "Main system prompt"
variables_required: [workspace]
---

You are a helpful coding assistant.

## Workspace
You operate in {{workspace}}. Read files via Read tool, edit
via Edit, search via Grep.

## Workflow
1. Plan before acting (use TodoCreate).
2. Read before editing.
3. Test after writing.

The whole markdown body becomes the agent's system_prompt. Any nested {{...}} placeholders (here {{workspace}}) are resolved recursively up to 10 levels deep, past which resolution errors out as a likely cycle.

{{skill.X}} - slash-command skill files​

Skills are reusable workflows the agent loads via use_skill. See Skills System. Two ways to ship them:

  1. Declared explicitly under dev.skills with {command, description, path}. The agent calls use_skill('/cmd') and gets the file content.
  2. Inlined via {{skill.X}} in another field (e.g. inside another agent's prompt). Same file, same content - different delivery path.
markdown
<!-- skills/commit.md -->

# /commit - Stage and push the diff

1. Run `git status`
2. Group changes by intent
3. Commit with conventional-commits messages
4. `git push`
yaml
dev:
skills:
- command: /commit
description: "Stage + commit + push"
path: skills/commit.md

{{behavior.X}} - custom behavior profiles​

Reference a YAML profile defined under behavior/. The file is parsed and returned as a JSON string the engine then loads:

yaml
security:
behavior:
profile: "{{behavior.strict_dev}}"

behavior/strict_dev.yaml:

yaml
name: strict_dev
description: "Ultra-strict dev rules"
extends: dev # build on top of the built-in dev profile

rules:
read_before_edit: true
test_after_changes: true
max_blind_reads: 1

prompt: |
Additional behavioral instructions appended to the agent's
system prompt.

custom:
- id: protect_migrations
rule: "Never modify migration files without asking"
trigger: edit
action: block

Resolution requires an actual YAML mapping - non-mapping content raises a clear error. Full Behavior Engine reference: Behavior Engine.

{{asset.X}} - asset URLs​

Returns a client-fetchable URL under /api/apps/{app_id}/assets/*, which the chat client / web client GETs directly.

yaml
ui:
greeting: |
Welcome! Here's what I can do:

![architecture]({{asset.docs/architecture.svg}})

{{asset.X}} resolves the placeholder's dotted path straight to a file under assets/ - {{asset.logo.svg}} → assets/logo.svg. There's no extension-guessing: the extension has to be in the placeholder (or the file itself has no extension and the placeholder doesn't include one), or resolution fails with "asset not found".

Path traversal is guarded - every resolved path must stay under <bundle>/assets/.

{{asset_b64.X}} - inlined base64​

Returns a data:<mime>;base64,<payload> URI. Useful for small icons embedded directly in HTML, SVG, or LLM prompts (avoids an HTTP round-trip from the client).

yaml
ui:
workspace:
title: "Editor"
# Inline a small icon directly into the system prompt
agents:
- id: assistant
system_prompt: |
![icon]({{asset_b64.icon}})
You are an assistant.

There is no size cap - the whole file is read and inlined as base64, so keep this for small icons and use {{asset.X}} (URL form) for anything large enough to bloat the compiled YAML.

The MIME type is sniffed from the file's own content (not its extension); an unrecognized type falls back to application/octet-stream.

{{include:path}} - YAML fragment inlining​

Inlines a YAML fragment file into the parent structure. Lets authors factor shared blocks between agents:

yaml
agents:
- id: main
brain: "{{include:fragments/main_brain.yaml}}"
- id: backup
brain: "{{include:fragments/main_brain.yaml}}"

fragments/main_brain.yaml:

yaml
provider: deepseek
model: deepseek-chat
backend: openai_compat
config:
api_key: "{{secret.DEEPSEEK_API_KEY}}"
temperature: 0.2
fallback:
provider: anthropic
model: claude-haiku-4-5
config:
api_key: "{{secret.ANTHROPIC_API_KEY}}"

The included file is parsed as YAML and returned as a structured object, so {{include:fragments/main_brain.yaml}} drops directly into a mapping field like brain:. Path traversal is guarded the same way as {{asset.X}} - resolved paths must stay under the bundle root. Recursion depth is the same 10-level cap shared with every other namespace resolver.

Auto-loaded directories (no explicit include: needed)​

Two directories are auto-loaded by the compiler with no YAML declaration:

DirectoryAuto-loaded into
agents/*.yamlAppended to agents: (each file = one agent definition)
hooks/*.yamlAppended to runtime.hooks

widgets/*.yaml is not auto-loaded - there is no directory convention for widgets, and no dev.include.widgets field either. Declare inline widgets directly under ui.widgets.inline in app.yaml, or pull one in with {{include:path}}.

Add more agents//hooks/ locations, or point at individual files instead of the whole directory, with an explicit dev.include block:

yaml
dev:
include:
agents: [./roster/triage.yaml, ./roster/refund.yaml]
hooks: ./shared/hooks/

When dev.include.agents is set, the convention auto-load is replaced (not merged with) the explicit list.

Compile-time guarantees​

Every namespace uses the same defensive coding:

  • Bundle-context required - when no bundle dir is set in the resolver context, the template may be left unresolved so callers can see the bad reference.
  • Path traversal blocked - resolved paths must stay inside the bundle; escapes are compile errors.
  • Missing files - compile errors list what is available when a referenced path cannot be resolved.
  • Recursion depth - capped; cycles raise a compile error.

Reloading after a bundle change​

The daemon does not watch the bundle for live edits. After you change prompts/, skills/, behavior/, assets/, fragments/, widgets/, or agents/, reinstall or reload the app:

bash
digitorn install ./my-app/app.yaml
# or, if already installed from the same path:
digitorn app-reload <app-id>

The next agent turn then uses the recompiled definition.

Cross-references​

  • Expressions (namespaces, the broader template syntax): Expressions
  • Skills system (where {{skill.X}} files come from): Skills System
  • Behavior profiles (where {{behavior.X}} files come from): Behavior Engine