An APP.md is a markdown file with YAML frontmatter that fully describes an agent-runnable app. Product-wise, an app is a portable capability surface: something a human, agent, CLI, UI, webhook, runtime, or another app can run, render, schedule, call, inspect, and verify. The frontmatter is the manifest; the body is the instructions the runtime hands to the agent.
---
{{ frontmatter — typed fields below }}
---
{{ markdown body — instructions for the agent }}
Parsers MUST reject any APP.md whose frontmatter is missing, unclosed, or fails schema validation. Public reference parser: scripts/validate.py. Reference runtime: backend/services/app_folder_service.py:load_from_folder in atrislabs/atrisos-backend. The frontmatter shape is normatively pinned by schema/app.v1.schema.json ($id: https://atris.ai/schema/app.v1.schema.json, JSON Schema Draft 2020-12), with schema/app.schema.json as the stable current-version alias. Where prose and schema diverge, prose normative rules in this file are authoritative; the schema is a machine-checkable subset of those rules (some rules — IANA timezone validity, RFC 8785 canonicalization, body content — fall outside JSON Schema's expressive range and live in the parser).
This spec describes the manifest contract, not every runtime behavior.
- Customers can package a capability once and connect it to a runtime, UI, CLI, scheduler, webhook, marketplace, or another app.
- Users and agents can run the app, inspect the manifest, call it through the surface a runtime exposes, review its outputs, and approve sensitive actions.
- Runtime builders decide how the app executes, renders, authenticates, bills, observes, and composes with other apps.
- App-to-app composition is represented in v1 through runtime surfaces such as
endpoints.*,capabilities,surfaces, MCP, webhooks, and the markdown body. Future versions may standardize richer dependency and receipt fields. - Receipt conventions live in
RECEIPTS.md: status, owner, verifier, decision, and learned fields are runtime conventions in v1, not required schema fields.
| Field | Type | Notes |
|---|---|---|
schema_version |
const 1 |
Bump on breaking change |
name |
string | Human-readable |
slug |
string | Identifier used in URLs, vaults, run keys. MUST match regex ^[a-z][a-z0-9]*(-[a-z0-9]+)*$ (lowercase letter to start; groups of [a-z0-9]+ joined by single hyphens; no leading digit, no underscores, no double hyphens, no trailing hyphen) and MUST be 1–64 characters long. The same grammar applies to member, skills[], and created_by_agent. |
access |
enum | private | business | public |
runtime |
enum | One of the 8 runtimes (see below) |
vault |
enum | atris-kms | app_secrets | byo-aws | local |
| Field | Type | Notes |
|---|---|---|
description |
string | Plain-text one-line summary of what the app does. Surfaced in registries, marketplaces, CLI listings, and search. Single-line; for longer copy use the markdown body. |
| Value | Where the code runs |
|---|---|
local |
Customer's machine as a subprocess |
subprocess |
Shared backend process (default for lightweight apps) |
ec2 |
Atris-managed EC2 sandbox |
webhook |
Customer-hosted HTTP endpoint |
external |
Full-stack external service (multiple endpoints) |
web |
Web app + optional MCP |
ios |
Native iOS via App Intents + deep links |
template |
Marketplace spec, no execution until installed |
Runtime-specific fields below tell the runtime how to actually execute. The required shape depends on runtime, and some runtimes require or allow more than one binding field. Two runtimes (local, template) are deliberately bindingless — see the No-binding runtimes section after the table.
| Field | Type | Required for | Notes |
|---|---|---|---|
block_pipeline_id |
uuid | optional, subprocess and ec2 only |
If absent, the runtime treats the manifest body + member + skills + secrets + remaining manifest context (wiki_paths, schedule, surfaces, etc.) as an implicit pipeline. Set explicitly to point at a compiled, version-pinned pipeline. Implementer rule: omitted ⇒ implicit; present ⇒ non-null UUID. Explicit null is rejected by Rule 4b ("known-but-misused"). |
endpoints.api |
URL | external, optional for webhook / web |
Primary HTTP API |
endpoints.frontend |
URL | web |
Where users see the UI |
endpoints.mcp |
URL | optional | MCP server URL |
endpoints.webhook |
URL | webhook |
Where the runtime POSTs |
endpoints.bundle_id |
string | ios |
iOS bundle id (com.example.app) |
endpoints.deep_link_scheme |
string | ios |
URL scheme |
auth.type |
enum | webhook, external, web if non-public |
none | api_key | bearer | hmac | oauth | jwt |
auth.issuer |
URL | when auth.type=oauth / jwt |
OIDC issuer |
auth.secret_ref |
non-empty string | when auth needs a key | Vault secret name |
capabilities |
list[string] | optional | Free-form labels for routers |
| none | — | local, template |
No execution-binding field is required or permitted; see below |
Validators MUST reject manifests that omit a required binding for their declared runtime, AND reject any executable binding (block_pipeline_id, endpoints.*) on runtime: template.
local: no execution-binding field is required. The markdown body of APP.md is treated as the agent system prompt; the runtime spawns an LLM subprocess on the customer's machine with shell access (the "agent + shell" model). Apps that need a deterministic local script are out of scope for v1; a future schema version may add deterministic local entrypoints.template: a manifest withruntime: templateMUST NOT include any execution-binding field (block_pipeline_id, anyendpoints.*, etc.). A template is a starting point to fork into a runnable app; the runtime cannot execute it directly. Forking flipsruntimetosubprocess/ec2/web/ etc. and adds the appropriate binding field.
| Field | Type | Notes |
|---|---|---|
secrets |
list[string] | Names only — values resolve from vault at run time |
schedule |
cron string | Optional. Standard 5-field, single-line cron. Default TZ is UTC; override with timezone: below. "0 7 * * *" = 07:00 daily in the resolved TZ. |
timezone |
IANA TZ string | Optional. e.g. America/Los_Angeles, Europe/London. Default UTC for every runtime (including local). When set, schedule is interpreted in this TZ. |
runtime_auth |
enum | How Atris authenticates the runtime back to itself: none | jwt | api_key. Default jwt |
- Cron expressions are standard 5-field, single-line strings (
min hour dom mon dow). Optional fields like seconds or year are not part of v1. - Default TZ for every runtime is UTC. Customer-machine local time is NOT a default — leaving
timezoneunset onruntime: localstill means UTC, to keep behavior identical across CI / dev / prod and stop the "works on my laptop, fires at 3am in CI" footgun. Apps that want customer-local firing MUST declaretimezone:explicitly. - DST handling (when
timezone:resolves to a TZ that observes DST):- Nonexistent local times (e.g.
02:30on the spring-forward morning) MUST be skipped. The runtime SHOULD log the skip for observability; specific event names are runtime-defined. - Repeated local times (e.g.
01:30on the fall-back morning) MUST fire once, not twice. The runtime SHOULD log the fold for observability.
- Nonexistent local times (e.g.
timezone:MUST be a value parsable by the IANA Time Zone Database (the runtime'stzdatasource). Validators MUST reject manifests with anEtc/GMT±Nstyle fixed offset that does not exist, or any free-form string that is not a valid IANA zone. Empty or missing → defaults to UTC (no error).timezone:was introduced inschema_version: 1. v1 parsers MUST allow it; failing-closed on it is a parser bug, not a manifest bug.
| Field | Type | Notes |
|---|---|---|
member |
slug | MEMBER.md to inherit persona / tools from |
skills |
list[slug] | SKILL.mds to load |
wiki_paths |
list[path] | Paths under workspace root that the runtime injects into the agent's context. E.g. wiki/customers/megan |
created_by_agent |
slug | The agent that authored this APP.md (provenance) |
artifact_dir |
path | Override for output writes. Defaults to {folder}/data/. Use when an app writes into a shared wiki path or mounted volume |
| Field | Type | Notes |
|---|---|---|
surfaces |
list[enum] | Where the app shows up: web | slack | mcp | voice | mobile | email | cli |
render |
enum | inline | embed | fullscreen | voice | none. Defaults to none |
ui_spec |
dict|null | Free-form layout / theme / component hints when render is not none. null means no UI contract is declared (future: typed React spec) |
events_schema |
dict|null | Declared shape of events this app emits to app_events. null means no event contract is declared; arbitrary JSON-compatible dict otherwise |
| Field | Type | Notes |
|---|---|---|
monetization.price_credits |
int ≥ 0 | Credits charged per successful run. 0 = free |
monetization.creator_share |
float 0..1 | Revenue share back to the manifest author |
monetization.stripe_connect_account |
string | acct_xxx — where the share lands |
Everything after the closing --- is the instructions block. The runtime injects it as the agent's working prompt at run time, alongside the resolved secrets, member persona, skills, wiki_paths, and any prior data/ state.
There's no required structure for the body; treat it as the briefing you'd give a competent operator: what the goal is, what to do when X, what the failure mode looks like.
A conforming parser:
- MUST reject APP.md without YAML frontmatter delimited by
---on its own line. - MUST validate
slugagainst the regex^[a-z][a-z0-9]*(-[a-z0-9]+)*$and a length of 1–64 characters. The regex is anchored and case-sensitive (Unicode and uppercase are implicitly rejected). No coercion (lowercasing, hyphen-collapsing, etc.) is permitted — invalid slugs are rejected, not normalized. 2a. MUST apply the same regex + length rule tomember, every item ofskills[], andcreated_by_agent. They are slug-typed; mismatched values reject the manifest. - MUST validate
runtimeis one of the 8 enum values. - Fail-closed scope — three rules that together replace the prior single "unknown = error" rule:
- 4a. MUST reject manifests missing any required field, or with a required field of the wrong type / enum value.
- 4b. MUST reject known-but-misused fields — e.g.
block_pipeline_idonruntime: web, any executable binding onruntime: template, an invalid IANA timezone, an unknown enum value for a known field. - 4c. Non-strict parsers MUST tolerate unknown frontmatter keys at the top level (warn, continue parsing) and MUST preserve unknown fields verbatim when re-emitting a manifest that parsed successfully. This is what lets a
schema_version: 1parser keep working when the spec adds new optional fields within v1 (e.g.description,timezone). Strict conformance mode (e.g. a CI lint flag) MAY reject unknown keys before re-emission; in strict mode preservation is not required on a failed parse. Strict mode is OPTIONAL and not part of the runtime contract. - Closed sub-schemas: rule 4c does NOT apply inside
secrets,auth,endpoints.*, ormonetization. These blocks have a fixed key shape and are treated as closed; unknown nested keys MUST be rejected. Adding new keys inside a closed block bumpsschema_version. - Open user-defined blocks:
events_schemaandui_specare open by design — their keys are app-defined (event names; layout hints). Parsers MUST NOT reject unknown keys inside these blocks. They are part of the manifest's contract, not the spec's.
- MUST treat
secretsas names only — never resolve values during parse. - MUST surface a stable
spec_digest— see Canonicalization for the exact algorithm. The digest lets callers detect drift between APP.md and any cached registration in a parser-agnostic way.
The spec_digest is SHA-256(canonical_record), lowercase hex. canonical_record is defined as follows.
Source. The parsed YAML frontmatter MUST map cleanly into the JSON value space (objects, arrays, strings, integers, floats, booleans, nulls). YAML's null spellings (~, null, empty) all normalize to JSON null via the parser. The markdown body — everything after the closing --- — is excluded from the digest: bodies are agent prompts that churn constantly during development. Body drift is tracked separately (file hash / git SHA / content hash).
Pre-canonicalization rejects. A parser MUST reject the manifest before computing a digest if any of these are present:
- Duplicate object keys at any nesting level.
- YAML tags (
!Foo), anchors (&a), or aliases (*a). - Non-JSON scalars (e.g. dates as YAML date type — represent as ISO 8601 strings instead).
NaN,+Infinity,-Infinity.- An unsupported
schema_version. Validate version before digesting; do not digest a manifest a parser cannot interpret.
Strings. Fed byte-for-byte into JCS. No Unicode normalization (NFC/NFD/NFKC/NFKD) is applied — two visually-identical strings with different code-point sequences MUST yield different digests.
JSON serialization (RFC 8785, JCS). Apply RFC 8785 to the tree:
- Object keys sorted lexicographically by Unicode code point, recursively.
- No insignificant whitespace.
- Strings written as literal UTF-8 (no
\uXXXXescapes for non-ASCII). - Integers as decimal digits with no leading zeros (sign prefixed
-). - Floats use the shortest round-trippable decimal per ECMAScript
Number.prototype.toString(RFC 8785 §3.2.2.3). true/false/nullfor booleans and null.[]empty array.{}empty object.
Exclusion. If the manifest contains a top-level spec_digest field, exclude it from the canonical record before serialization (so an app can store its own digest without recursion). Nested spec_digest keys are not excluded.
Forward-compat. Unknown top-level keys preserved by Rule 4c DO participate in the digest. The digest reflects the actual manifest, not the spec's known-keys subset.
Implementation. Implementations MUST conform to RFC 8785; conformance tests SHOULD pin a specific JCS library and version. The algorithm itself fits in ~60 lines for languages without a JCS library — the worked example below is a sufficient self-test for any implementation.
Worked example.
---
schema_version: 1
name: x
slug: x
access: private
runtime: local
vault: local
---Canonical bytes:
{"access":"private","name":"x","runtime":"local","schema_version":1,"slug":"x","vault":"local"}
Expected digest: eb9beb40790eeab0329641e230043e058e5819dfb5d526e81e7997af35b978a3.
A parser whose spec_digest differs from this on the example above is non-conforming.
This is schema_version: 1. Backwards-compatible field additions are limited to new optional top-level fields that Rule 4c parsers can warn-and-continue on while preserving them on re-emit. Anything that requires existing parsers to change (renamed required field, removed enum value, new enum value, semantic shift, additions inside a closed sub-schema) bumps to schema_version: 2 and ships a migration note in CHANGELOG.md.
The public v1 reference parser validates only schema_version: 1; future parsers that support multiple schema versions MUST dispatch validation by the manifest's declared schema_version.