App Configuration
The YAML file has 6 top-level blocks. Only app: and agents: are required.
YAML Structure
app: # Required — application identity
variables: # Optional — template variables
modules: # Optional — module configuration
agents: # Required — agent definitions (list)
channels: # Optional — output channel instances
execution: # Optional — runtime configuration
capabilities: # Optional — security configuration
App Block
app:
app_id: my-app # Required. Unique identifier
name: "My Application" # Required. Human-readable name
version: "1.0" # Version string (default: "1.0")
description: "What this app does" # Optional description
author: "your-name" # Optional author
tags: [coding, assistant] # Searchable tags
Field Reference
| Field | Type | Default | Description |
|---|---|---|---|
app_id | string | required | Unique application identifier |
name | string | required | Human-readable name |
version | string | "1.0" | Version string |
description | string | "" | Description |
author | string | "" | Author name |
tags | list[string] | [] | Searchable tags |
Variables
The variables: block defines reusable values accessible throughout the YAML via {{variable_name}}.
variables:
workspace: "{{env.PWD}}"
max_file_lines: 500
api_token: "{{env.MY_API_KEY}}"
Accessing Variables
Variables are resolved in all blocks of the YAML:
modules.*.config— module configuration valuesmodules.*.setup[].params— setup step parametersmodules.*.constraints— constraint valuesagents[].brain.config— provider config (api_key, base_url, etc.)agents[].system_prompt— system prompt textexecution.greeting— greeting messageexecution.workspace— working directory
agents:
- id: assistant
system_prompt: |
Working directory: {{workspace}}
Max lines: {{max_file_lines}}
Built-in Variables
| Variable | Description |
|---|---|
{{env.VAR_NAME}} | Environment variable |
{{secret.VAR_NAME}} | Per-app secret (DB first, env fallback) |
Secrets ({{secret.XXX}})
The {{secret.XXX}} syntax resolves secrets with a two-step lookup:
- Database — checks the per-app encrypted secret store first
- Environment — falls back to
os.environ(backward compatible)
This means you can start with environment variables and migrate to DB secrets without changing your YAML.
Storing secrets in the database
# Via CLI (daemon must be running)
digitorn secret set my-app API_KEY "sk-live-abc123"
digitorn secret set my-app API_KEY # prompts for value (hidden input)
digitorn secret list my-app # list keys (values never shown)
digitorn secret delete my-app API_KEY
# Via API
curl -X PUT http://localhost:8000/api/apps/my-app/secrets/API_KEY \
-H "Content-Type: application/json" \
-d '{"value": "sk-live-abc123"}'
Secrets are encrypted at rest with Fernet (AES-128-CBC + HMAC-SHA256), isolated per app, and never returned in plaintext by the API.
Usage in YAML
modules:
mcp:
config:
servers:
notion:
auth:
client_id: "{{secret.NOTION_CLIENT_ID}}"
client_secret: "{{secret.NOTION_CLIENT_SECRET}}"
agents:
- id: assistant
brain:
config:
api_key: "{{secret.OPENAI_API_KEY}}"
Modules Block
The modules: block declares which modules to load and configures them.
modules:
# Empty config — just load the module
hello: {}
# With constraints
filesystem:
constraints:
allowed_actions: [read, ls, find, grep]
# With config and setup steps
database:
config:
timeout_seconds: 10
setup:
- action: connect
params:
connection_id: main
driver: sqlite
database: "{{workspace}}/data.db"
constraints:
allowed_actions: [fetch_results, list_tables]
blocked_actions: [execute_query]
ModuleBlock Fields
| Field | Type | Default | Description |
|---|---|---|---|
config | dict | {} | Static module configuration, pushed via on_config_update() at bootstrap |
setup | list[SetupStep] | [] | Ordered actions executed at bootstrap time |
constraints | dict | {} | Runtime restrictions (allowed_actions, blocked_actions, module-specific) |
SetupStep Fields
| Field | Type | Default | Description |
|---|---|---|---|
action | string | required | Action name on the module |
params | dict | {} | Parameters (may contain {{variables}}) |
Currently Implemented Modules
| Module | Description |
|---|---|
hello | Simple greeting module (test/demo) |
filesystem | File read, list, find, grep, write, mkdir operations |
database | Multi-driver database operations (SQLite, PostgreSQL, MySQL, MSSQL, Oracle, MongoDB, Redis) |
http | HTTP client: GET, POST, JSON API, page fetch, download with progress tracking |
shell | Shell execution: run commands, scripts, background processes, env/which |
llm_provider | LLM provider management (auto-configured from brain) |
context_builder | Tool discovery engine (system module, auto-loaded) |
Note: The
context_buildermodule is loaded automatically — you never declare it inmodules:. Thellm_providermodule is auto-configured from thebrain:block in each agent.
Setup Steps and Pre-Configured Resources
When a module has setup: steps, they are executed at bootstrap time (app startup). The runtime automatically summarizes all successful setup steps and injects them into the agent's system prompt under a # PRE-CONFIGURED RESOURCES section.
This means the agent knows what's already configured without having to discover it. For example, with:
modules:
database:
setup:
- action: connect
params:
connection_id: main_db
driver: postgresql
host: db.example.com
database: myapp
password_env: DB_PASSWORD
The agent's system prompt will include:
# PRE-CONFIGURED RESOURCES
The following resources were set up at startup and are ready to use:
- database.connect | connection_id=main_db | driver=postgresql | host=db.example.com | database=myapp | password_env=***
You do NOT need to configure these again — use them directly.
Sensitive fields (password, password_env, api_key, secret, token) are automatically redacted. If no module has setup steps, this section is not injected.
Auto-Schema Injection (Database)
When the database module has active connections (from setup steps), the runtime automatically introspects all connected databases and injects the full schema into the agent's system prompt. The agent knows the table structure from the first message — no tool calls needed to discover the schema.
The schema includes:
- Table names and DB-native comments (
COMMENT ONin PostgreSQL, column comments in MySQL) - Column names, types, constraints (PK, NOT NULL)
- Foreign key relationships
- Business annotations (from YAML
annotatesteps — see below)
Example system prompt injection:
DATABASE SCHEMA:
[main_db] (postgresql)
users — Registered platform users
- id INTEGER PK NOT NULL
- name TEXT NOT NULL
- email TEXT NOT NULL — Primary email, unique, used for authentication
- created_at TIMESTAMP NOT NULL — Registration date
FK: team_id -- teams.id
orders — Customer orders
- id INTEGER PK NOT NULL
- user_id INTEGER NOT NULL — References users.id
- total DECIMAL NOT NULL — Order total in cents
- status TEXT NOT NULL — pending|confirmed|shipped|delivered
Business Annotations
Use the annotate setup step to add business context to tables and columns. Annotations are prioritized over DB-native comments and give the agent a deep understanding of the data model.
modules:
database:
setup:
- action: connect
params:
connection_id: main_db
driver: postgresql
host: "{{env.DB_HOST}}"
database: myapp
password_env: DB_PASSWORD
policy:
preset: safe_write
# Table-level annotation
- action: annotate
params:
connection_id: main_db
table: users
description: "Registered platform users — one row per account"
tags: [core, pii]
# Column-level annotations
- action: annotate
params:
connection_id: main_db
table: users
column: email
description: "Primary email, unique, used for login and notifications"
tags: [pii, unique]
- action: annotate
params:
connection_id: main_db
table: orders
column: status
description: "Order lifecycle: pending -- confirmed -- shipped -- delivered"
tags: [enum]
Annotation fields:
| Field | Type | Description |
|---|---|---|
description | string | Business description (prioritized over DB comment) |
tags | list[string] | Searchable tags (e.g. pii, financial, immutable) |
glossary | dict | Business glossary (e.g. {"SKU": "Stock Keeping Unit"}) |
rules | list[string] | Business rules (e.g. "status transitions are one-way") |
Priority: YAML annotation > DB-native comment > empty. If the database already has COMMENT ON (PostgreSQL) or column comments (MySQL), they are used as fallback when no YAML annotation exists.
Database High-Level Actions
In addition to execute_query and fetch_results, the database module provides optimized actions for common operations:
| Action | Risk | Description |
|---|---|---|
bulk_insert | medium | Insert multiple rows in one call. Provide columns + rows (array of arrays). Atomic transaction. |
batch_execute | high | Execute multiple SQL statements in a single atomic transaction. All succeed or all roll back. |
upsert | medium | Insert or update rows. If conflict_columns match an existing row, it updates instead of failing. |
These actions are much faster than calling execute_query repeatedly — they reduce tool calls from N to 1 and use transactional batching.
Upsert Example
# The agent calls upsert with:
{
"connection_id": "main_db",
"table": "users",
"columns": ["email", "name", "status"],
"rows": [
["alice@example.com", "Alice Updated", "active"],
["bob@example.com", "Bob New", "pending"]
],
"conflict_columns": ["email"],
"update_columns": ["name", "status"]
}
Generates driver-appropriate SQL:
- SQLite/PostgreSQL:
INSERT ... ON CONFLICT (email) DO UPDATE SET name=EXCLUDED.name, status=EXCLUDED.status - MySQL:
INSERT ... ON DUPLICATE KEY UPDATE name=VALUES(name), status=VALUES(status) - MSSQL:
MERGE ... WHEN MATCHED THEN UPDATE ... WHEN NOT MATCHED THEN INSERT ...
All high-level actions enforce the same security layers: QueryGuard policy, table/column access control, audit logging, and transaction timeouts.
Module Constraints
Universal constraints available for any module:
| Constraint | Type | Description |
|---|---|---|
allowed_actions | list[string] | Whitelist of allowed action names |
blocked_actions | list[string] | Blacklist of blocked action names |
Modules may declare additional constraints via their ConstraintSpec — use digitorn app schema {module_id} to see them.
Discovering Module Schemas
Use the CLI to see what's available:
# List all available modules
digitorn app schema hello
# Shows:
# - All actions with their parameter schemas
# - All supported constraints
# - Config fields (if any)
# - YAML template for quick copy-paste
Channels Block
The channels: block declares named output channel instances for delivering notifications from scheduled jobs, watchers, and background tasks. Channels are the notification delivery infrastructure — they route results to external systems (Slack, email, Kafka, webhooks, etc.).
channels:
slack_alerts:
type: webhook
config:
url: "{{env.SLACK_WEBHOOK_URL}}"
headers:
Content-Type: "application/json"
audit_log:
type: log
config:
logger_name: "digitorn.audit"
level: "INFO"
format: json
include_data: true
Channel Instance Fields
| Field | Type | Default | Description |
|---|---|---|---|
type | string | required | Channel type ID: webhook, log, or any installed plugin (slack, telegram, etc.) |
config | dict | {} | Channel-specific configuration (supports {{variables}} and {{env.VAR}}) |
user_resolver | object | null | Optional auto-resolution of per-user delivery targets (email, phone, chat_id) from a data source. See Per-User Channel Resolution |
user_resolver.module | string | required | Module ID to query (e.g. database, http) |
user_resolver.action | string | required | Action to call on the module |
user_resolver.params | dict | {} | Action parameters (:session_id is replaced with the user's session) |
user_resolver.mapping | dict | {} | Maps result fields to per-delivery config fields |
user_resolver.cache_ttl | float | 300 | Cache duration in seconds (0 = no cache) |
Built-in Channel Types
| Type | Description |
|---|---|
llm_notification | Push to agent conversation (always available, no config needed) |
webhook | HTTP POST to any URL (Slack, Discord, Teams, Zapier, n8n compatible) |
log | Structured Python logging (debugging, audit trails) |
Plugin channels are installed via pip (pip install digitorn-channel-slack) and auto-discovered. See Output Channels for the full channel system documentation.
Execution Block
The execution: block configures runtime behavior.
execution:
mode: conversation # 'one_shot', 'conversation', or 'background'
greeting: "Hello!" # Greeting message (conversation mode)
max_turns: 10 # Maximum agent loop iterations
timeout: 120.0 # Total timeout in seconds
entry_agent: assistant # Which agent starts (multi-agent)
workspace: "{{workspace}}" # Working directory for file operations
context: # Default context management for all agents
max_tokens: 0
strategy: summarize
compression_trigger: 0.75
hooks: [] # Custom hooks (see Context Management)
triggers: [] # Triggers for background mode
watchers: false # Enable persistent monitoring (watch_* primitives)
scheduler: false # Enable scheduler + remember primitives
default_channel: llm_notification # Default output channel for jobs/watchers
Execution Fields
| Field | Type | Default | Description |
|---|---|---|---|
mode | string | "one_shot" | Execution mode: one_shot, conversation, or background |
entry_agent | string | "" | Agent to start with. Empty = first agent in list |
max_turns | int | 50 | Max agent loop iterations (per turn for conversation, per activation for background) |
timeout | float | 300.0 | Timeout in seconds (per turn for conversation, per activation for background) |
greeting | string | "" | Greeting message displayed at conversation start |
workspace | string | "" | Working directory for file operations. Resolution: (1) explicit value in YAML, (2) parent directory of the YAML source file, (3) CLI mode: current working directory, (4) daemon mode: managed directory under ~/.local/share/digitorn/workspaces/{app_id}/ |
input | InputConfig | InputConfig() | Input contract (one_shot mode only) |
output | OutputConfig | OutputConfig() | Output contract (one_shot mode only) |
context | ContextConfig | ContextConfig() | Default context management for all agents (see Context Management) |
hooks | list[HookConfig] | [] | Custom hooks (see Context Management) |
watchers | bool | false | Enable persistent monitoring. When true, the agent gets watch_* primitives for periodic data source monitoring with smart escalation (see Execution Primitives) |
scheduler | bool | false | Enable time-based scheduling. When true, the agent gets schedule_once, schedule_cron, schedule_cancel, schedule_list, schedule_status, and remember primitives (see Execution Primitives) |
default_channel | string | "llm_notification" | Default output channel for scheduled jobs and watchers. Must reference a channel instance name from the channels: block, or "llm_notification" (always available). See Output Channels |
triggers | list[TriggerConfig] | [] | Triggers for background mode |
Execution Modes
| Mode | Description |
|---|---|
one_shot | Process a single input and return. Uses input/output contracts. |
conversation | Interactive multi-turn conversation. Uses greeting, max_turns. |
background | Daemon mode, activated by triggers (cron, file watch). Uses triggers. |
Input/Output Contracts (one_shot mode)
Define what your application accepts and produces.
Input types:
| Type | Description | Model requirement |
|---|---|---|
text | Plain text (default) | All models |
image | Image file (PNG, JPEG, WebP) | Vision models (GPT-4o, Claude Sonnet, Gemini) |
audio | Audio file (WAV, MP3, M4A) | Audio models (GPT-4o-audio, Gemini) |
video | Video file (MP4) | Gemini |
file | Any file (read via filesystem module) | All models |
json | Structured JSON input | All models |
any | Text, images, or files | Depends on model |
Output types:
| Type | Description | CLI behavior |
|---|---|---|
text | Plain text | Printed to stdout |
json | Structured JSON | Pretty-printed, validated against schema |
markdown | Markdown text | Rendered with Rich (headers, code blocks, tables) |
file | File written to disk | Path printed to stdout |
image | Generated image | Saved to file, path printed |
audio | Generated audio | Saved to file, path printed |
Examples:
# Text analysis with JSON output
execution:
mode: one_shot
input:
type: text
description: "Code to analyze"
required: true
output:
type: json
description: "Analysis report"
schema:
type: object
properties:
bugs: { type: array }
score: { type: integer }
# Image analysis
execution:
mode: one_shot
input:
type: image
accept: ["image/png", "image/jpeg", "image/webp"]
max_size: "10MB"
description: "Image to analyze"
output:
type: json
description: "Detected objects and description"
# Audio transcription
execution:
mode: one_shot
input:
type: audio
accept: ["audio/wav", "audio/mp3", "audio/m4a"]
max_size: "50MB"
description: "Audio to transcribe"
output:
type: text
description: "Transcription"
# Conversation with image support
execution:
mode: conversation
input:
type: any
accept: ["image/png", "image/jpeg", "application/pdf"]
description: "Text or images"
# Code generator with file output
execution:
mode: one_shot
input:
type: text
description: "Description of what to generate"
output:
type: file
format: ".py"
description: "Generated Python file"
Input fields:
| Field | Type | Default | Description |
|---|---|---|---|
type | string | "text" | Input type (text, image, audio, video, file, json, any) |
accept | list | [] | Accepted MIME types. Empty = infer from type |
max_size | string | "" | Max input size (e.g. "10MB"). Empty = no limit |
description | string | "" | Human-readable description |
required | bool | true | Whether input is mandatory |
Output fields:
| Field | Type | Default | Description |
|---|---|---|---|
type | string | "text" | Output type (text, json, markdown, file, image, audio) |
format | string | "" | Format hint (.py, .svg, png, etc.) |
description | string | "" | Human-readable description |
schema | object | {} | JSON Schema for output validation (json type only) |
Workspace
The workspace field sets the working directory for file operations. It supports template variables:
variables:
workspace: "{{env.PWD}}"
execution:
workspace: "{{workspace}}"
If not set explicitly, the workspace defaults to: explicit value > YAML source file's parent directory > current working directory.
Background Mode and Triggers
Background mode turns the app into a daemon that reacts to events. Each trigger activates the agent with a message.
execution:
mode: background
triggers:
# Run every hour
- id: hourly_check
type: cron
schedule: "0 * * * *"
message: "Hourly check: analyze recent changes."
# Watch for new files
- id: new_csv
type: watch
paths: ["./inbox/*.csv"]
message: "New file detected: {{event.path}}"
Trigger Types
| Type | Required Fields | Description |
|---|---|---|
cron | schedule | Cron expression (e.g., "0 * * * *" = every hour) |
watch | paths | File glob patterns to watch for changes |
http | path, method | HTTP endpoint trigger (not yet implemented) |
TriggerConfig Fields
| Field | Type | Default | Description |
|---|---|---|---|
id | string | required | Unique trigger identifier |
type | string | required | Trigger type: cron, watch, or http |
schedule | string | "" | Cron expression (cron type only) |
paths | list[string] | [] | File glob patterns (watch type only) |
path | string | "" | HTTP endpoint path (http type only) |
method | string | "POST" | HTTP method (http type only) |
message | string | "" | Message sent to the agent when triggered |
Note: HTTP triggers are defined in the schema but not yet implemented in the runtime.
Complete Example
This example uses all 6 top-level blocks and demonstrates most configuration options: variables with environment references, modules with config/setup/constraints, brain with context management and summary brain, execution with workspace and hooks, and capabilities with grants/denials.
app:
app_id: data-analyst
name: "Data Analyst"
version: "2.0"
description: "AI data analysis assistant with read-only access."
author: "digitorn"
tags: [data, analysis, sql]
variables:
workspace: "{{env.PWD}}"
db_path: "{{workspace}}/data.db"
api_key: "{{env.DEEPSEEK_API_KEY}}"
modules:
hello: {}
filesystem:
constraints:
allowed_actions: [read, ls, find, grep]
database:
config:
timeout_seconds: 30
setup:
- action: connect
params:
connection_id: main
driver: sqlite
database: "{{db_path}}"
constraints:
allowed_actions: [fetch_results, list_tables]
agents:
- id: analyst
role: assistant
brain:
provider: deepseek
model: deepseek-chat
backend: openai_compat
temperature: 0.2
max_tokens: 8192
config:
api_key: "{{api_key}}"
context:
max_tokens: 80000
output_reserved: 4096
strategy: summarize
keep_recent: 10
compression_trigger: 0.75
summary_max_tokens: 1024
auto_compact: true
summary_brain:
provider: ollama
model: qwen2.5:3b
backend: openai_compat
system_prompt: |
You are a data analyst. Query databases and read files.
Workspace: {{workspace}}
WORKFLOW:
1. list_categories -> see available modules
2. browse_category(category="name") -> see module tools
3. execute_tool(name="module.action", params={...}) -> execute
IMPORTANT:
- Go directly to execute_tool once you know the tool name.
- Limit yourself to 3-5 tool calls per question.
- If a tool fails, explain the error instead of retrying.
execution:
mode: conversation
greeting: "Data Analyst ready. Ask me about your data."
workspace: "{{workspace}}"
max_turns: 40
timeout: 600.0
hooks:
- id: pressure_log
on: turn_start
condition:
type: always
action:
type: log
message: "Turn {turn}: ~{tokens} tokens, {messages} messages"
cooldown: 0
capabilities:
default_policy: auto
max_risk_level: low
grant:
- module: filesystem
actions: [read, ls, find, grep]
- module: database
actions: [fetch_results, list_tables]
- module: hello
deny:
- module: filesystem
actions: [write, delete]
reason: "Read-only mode"
- module: database
actions: [execute_query]
reason: "Only fetch_results allowed"