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
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
| Pattern | Folder | Resolves to |
|---|---|---|
{{prompt.X}} | prompts/X.md | File content (raw markdown) |
{{skill.X}} | skills/X.md | File content |
{{behavior.X}} | behavior/X.yaml | Parsed YAML, returned as a JSON string |
{{asset.X}} | assets/X | URL: a stable asset URL for that app |
{{asset_b64.X}} | assets/X | data:<mime>;base64,<payload> URI |
{{include:path}} | <bundle>/path | Parsed 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:
.md.markdown.txt.prompt- 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:
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:
---
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.
agents:
- id: assistant
brain: { ... }
system_prompt: "{{prompt.assistant_system}}"
prompts/assistant_system.md:
---
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:
- Declared explicitly under
dev.skillswith{command, description, path}. The agent callsuse_skill('/cmd')and gets the file content. - Inlined via
{{skill.X}}in another field (e.g. inside another agent's prompt). Same file, same content - different delivery path.
<!-- 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`
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:
security:
behavior:
profile: "{{behavior.strict_dev}}"
behavior/strict_dev.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.
ui:
greeting: |
Welcome! Here's what I can do:

{{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).
ui:
workspace:
title: "Editor"
# Inline a small icon directly into the system prompt
agents:
- id: assistant
system_prompt: |

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:
agents:
- id: main
brain: "{{include:fragments/main_brain.yaml}}"
- id: backup
brain: "{{include:fragments/main_brain.yaml}}"
fragments/main_brain.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:
| Directory | Auto-loaded into |
|---|---|
agents/*.yaml | Appended to agents: (each file = one agent definition) |
hooks/*.yaml | Appended 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:
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:
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