The bundle
The folder that holds an app's definition, and every option authoric.json takes.
A bundle is the folder in your repository that holds an app's definition: authoric/ by
default, or whichever folder you chose when you connected the repository. It always starts with an
authoric.json.
authoric/
authoric.json # the root file: format version, what the bundle manages, layout
deno.json # optional: import map for the handlers
deno.lock # required whenever deno.json names a remote dependency
tables/
vendors.yaml # one file per table, named for it
invoices.yaml
roles/
approver.yaml # one file per role, named by its key
policies/
refunds_small.yaml # one file per policy, named by its key
actions/
orders.yaml # a table's custom actions, named for the table
agents/
refunder.yaml # one file per agent, named by its key
jobs/
nightly_dunning.yaml # one file per scheduled job, named by its key
storages/
invoices.yaml # one file per file storage, named by its key
settings.yaml # the app's brand, and the connections it may use
environments/
staging.yaml # what differs for the app whose environment is staging
handlers/
_shared/money.mjs # shared modules, copied into every handler
refund_order/
index.mjs # one folder per handler; index.mjs is its entrypoint
nightly_dunning/
index.mjsA bundle is read strictly. A file or folder the bundle does not know, a key authoric.json does not
know, or a value of the wrong shape fails the deployment with the file and the fix, before anything
changes. A typo never half-applies.
authoric.json
{
"$schema": "https://authoric.ai/schema/authoric-v1.json",
"formatVersion": 1,
"manage": ["handlers"],
"functionsRoot": "handlers",
"entrypoint": "index.mjs",
"shared": ["_shared"],
"handlers": {
"refund_order": {
"entryAction": "orders:refund",
"description": "Refunds an order up to its paid amount."
},
"nightly_dunning": { "kind": "worker" },
"fixtures": { "exclude": true }
}
}| Key | Default | Meaning |
|---|---|---|
formatVersion | required | The format of the bundle. The current format is 1. |
manage | ["handlers"] | Which kinds of object the bundle owns: tables, storages, roles, policies, actions, agents, jobs, settings, handlers. Only a listed kind is managed by the repository, so handing a kind over to code is a reviewed line in a pull request, and a folder for a kind the bundle does not list is refused. |
functionsRoot | "handlers" | The folder, inside the bundle, that holds one folder per handler. "" means the bundle root. |
entrypoint | "index.mjs" | The file in each handler folder that exports the handler. |
shared | ["_shared"] | Folders, inside functionsRoot, copied into every handler so ../_shared/… imports resolve. A shared folder is never a handler itself. |
handlers | {} | Settings per handler folder, below. |
Per handler folder:
| Key | Meaning |
|---|---|
entrypoint | This handler's entrypoint file, instead of the default. |
entryAction | The action the handler backs. Defaults to <name>:execute. |
kind | endpoint (default), table_action, worker (job-only), sdk or other. |
description | One line saying what the handler does, shown wherever a handler is picked. At most 500 characters. |
shared | This handler's shared folders, replacing the list above for this handler only. |
exclude | true keeps a folder with an entrypoint from becoming a handler. |
A handler is named after its folder. A folder name that is not a valid handler name is turned into
one (refund-order becomes refund_order), and two folders that would become the same name fail the
deployment.
Tables
One file per table, tables/<name>.yaml, named for the table. List "tables" in manage to use
them.
description: Vendors we pay.
scopeMode: shared # or scoped: each row belongs to one of the app's organizations
deleteStrategy: soft # or hard
locked: [delete] # REST operations closed on this table
columns:
- name: code
type: varchar(32)
- name: display_name
type: varchar(190)
renamedFrom: name # rename, keeping the data
- name: payment_terms
type: enum('net_15','net_30','net_60')
default: net_30
- name: bank_account
type: varchar(64)
nullable: true
redact: true # never returned in reads
indexes:
- columns: [code]
unique: true
foreignKeys:
- columns: [parent_vendor_id]
references: { table: vendors, columns: [id] }
onDelete: restrict| Key | Default | Meaning |
|---|---|---|
description | What the table holds. | |
scopeMode | shared | scoped: each row belongs to one of the app's organizations. shared: every organization sees every row. |
deleteStrategy | hard | soft keeps deleted rows, marked deleted. |
locked | [] | REST operations closed on the table: create, read, read_list, update, delete. |
allowWrite | true | Whether the console's data explorer may edit rows. |
columns | required | The table's own columns, in order. |
indexes | [] | Each with columns, and unique: true for a unique index. |
foreignKeys | [] | Each with columns, references (table, columns), and onDelete, onUpdate: restrict (default), cascade, set null or no action. |
Per column:
| Key | Default | Meaning |
|---|---|---|
name | required | Lowercase letters, digits and underscores. |
type | required | The MySQL type: varchar(190), char(2), int, bigint unsigned, decimal(12,2), boolean, text, json, datetime, date, enum('a','b'). |
nullable | false | Whether the column may be empty. |
default | The value a new row gets. | |
description | What the column means. | |
redact | false | Never return the column's values in reads. |
renamedFrom | The column's old name. A rename keeps the column's data; without it, a new name is a new column and the old one is deleted. Rename in one deployment and change the column in the next. |
Every table also has the platform's own columns (id, uid, org_id for a scoped table, and the
timestamps). They are added for you; declaring one is refused.
How each change is classified:
| Change | Group |
|---|---|
| A new table, column or index | Adds |
A wider type (a longer varchar, int to bigint, more enum values), a rename, a new order, a new foreign key | Changes |
A column that is no longer redacted, a table that becomes shared, a REST operation that opens | Widens authority |
| A type that may not keep every value, a column made required while some rows are empty, a deleted column, a deleted table | Removes (always waits for an approver, saying how many rows have a value) |
Tables are created after the tables their foreign keys reference, and deleted before. A table made in the console that the bundle does not name is left alone.
Roles and policies
List "roles" and "policies" in manage to keep an app's access in the repository. A policy is
its stored definition, unchanged; a role says which policies it carries.
# policies/refunds_small.yaml
name: Refunds up to $1,000
definition:
version: "1.0"
defaultEffect: deny
actions:
refunds:create:
- effect: allow
priority: 100
when: { "arg.amount": { lte: 1000 } }# roles/approver.yaml
display: Approver
policies:
- policy: refunds_small
priority: 100| Key | Meaning |
|---|---|
name (policy) | The policy's name, shown in the console. |
active (policy) | false keeps the policy but turns it off. Default true. |
definition (policy) | version, defaultEffect (deny by default) and actions: each action key with its rules (effect, priority, when, description). See Access control. |
display (role) | The role's name, shown in the console. |
policies (role) | The policies it carries, by key, each with a priority (default 100). |
The file name is the key: policies/refunds_small.yaml is the policy refunds_small in every app
that deploys the bundle. A built-in role (such as User) is never defined by a bundle, only
extended: its file lists the policies to attach, and nothing else. A bundle may also attach one of
the platform's own policies by its key. Who holds a role stays in the app.
How each change is classified:
| Change | Group |
|---|---|
| A new policy, a new role | Adds |
| A role that carries a policy, a policy that allows another action or changes a condition, a policy turned back on | Widens authority (the approver must hold what it allows) |
| A policy that allows less, is turned off or is deleted; a policy a role no longer carries | Narrows |
| A deleted role | Removes (its holders lose it) |
The bundle owns the bindings of the policies it manages. A policy made in the console stays on whatever roles it is on.
Custom actions
A table's custom actions live in actions/<table>.yaml. List "actions" in manage to use them.
# actions/orders.yaml
actions:
- method: post
path: /actions/refund
description: Refund an order up to its paid amount.
handler: refund_order
allowedActions: [orders:read, refunds:create]
schema:
- { column_name: amount, data_type: decimal, is_nullable: "NO" }
agentTool:
name: refundOrder
risk: high| Key | Meaning |
|---|---|
method, path | The route, on its table: together they name the action. |
description | What the action does. |
handler | The handler that runs it, by name: one in the bundle or already in the app. |
allowedActions, denyActions | What the handler may do while it runs for this action (its envelope). |
schema | The action's input fields, in the console's field format. |
agentTool | Offers the action to agents: name, description and risk (low, moderate, high). |
How each change is classified:
| Change | Group |
|---|---|
| A new action that delegates anything, an envelope that allows more, an action offered to agents | Widens authority (the approver must hold what it delegates) |
| A new action that delegates nothing | Adds |
| A new handler, schema or description | Changes |
| An envelope that allows less, an action removed or no longer offered to agents | Narrows |
Actions apply after the handlers they name, so a bundle can add a handler and the action that runs it in one deployment.
Agents
The app's own agents live in agents/<key>.yaml. List "agents" in manage to use them.
# agents/refunder.yaml
name: Refunder
description: Refunds an order when the customer is owed one.
effort: short
roles: [refund_operator]
llm:
connection: Production OpenAI # the AI connection's label in the console
autoApprove: false| Key | Default | Meaning |
|---|---|---|
name | required | The agent's name. |
description | Its instructions. | |
effort | short | brief, short, moderate, long or max. |
roles | [] | The roles it holds, by key (every agent also holds the built-in agent role). |
llm | the platform's AI | connection: the label of one of your AI connections; model: optional. |
autoApprove | false | Approve its own steps instead of pausing for a person. |
autoApproveBudget | false | Go past its effort budget without asking. |
An agent's on/off switch and its organization reach stay in the console: a deployment never turns an agent on or off, and only the owner, signed in, changes what organizations it reaches.
How each change is classified:
| Change | Group |
|---|---|
| A role the agent gains, approving its own steps, going past its budget without asking | Widens authority (the approver must hold what the role allows) |
| A new agent that holds nothing | Adds |
| A new name, instructions, effort or AI connection | Changes |
| A role it loses, pausing for a person again | Narrows |
| A deleted agent | Removes (its API keys go with it) |
Scheduled jobs
The app's scheduled jobs live in jobs/<key>.yaml. List "jobs" in manage to use them.
# jobs/nightly_dunning.yaml
name: Nightly dunning
handler: nightly_dunning # the handler it runs, by name
schedule:
cron: '0 2 * * *' # or every: 3600 (seconds)
timezone: America/New_York
payload: { batch: 50 }
timeoutSeconds: 300
roles: [biller]| Key | Default | Meaning |
|---|---|---|
name | required | The job's name. |
description | What it is for. | |
handler | required | The handler it runs. It must exist in the app or in the same bundle. |
schedule | required | every (seconds) or cron (minute hour day month weekday), never both, with an optional timezone. |
payload | {} | What the handler receives. |
timeoutSeconds | 300 | How long one turn may run, up to 3,600. |
roles | [] | The roles the job's principal holds, by key. |
A job is born paused. A deployment creates it stopped and never starts or stops one: starting a job is a person's act in the console, and a later deployment leaves it running or paused as it was.
| Change | Group |
|---|---|
| A role the job gains | Widens authority (the approver must hold what the role allows) |
| A new job | Adds |
| A new name, handler, schedule, payload or timeout | Changes |
| A role it loses | Narrows |
| A deleted job | Removes |
File storages
The app's file storages live in storages/<key>.yaml. List "storages" in manage to use them.
# storages/invoices.yaml
name: Invoices
visibility: private # or public
maxFileBytes: 10485760
allowedMimeTypes: [application/pdf, image/png]| Key | Default | Meaning |
|---|---|---|
name | required | The storage's name. |
visibility | private | public lets anyone with a file's link read it. |
maxFileBytes | the plan's limit | The largest file it accepts. |
allowedMimeTypes | any type | The file types it accepts. |
The file declares the storage, never its contents: folders, files and sharing are data and stay in the app. Where the files are kept is the app's own placement and never part of a bundle.
| Change | Group |
|---|---|
| A storage made public, or a new public storage | Widens authority |
| A new private storage | Adds |
| A new name, size limit or file types | Changes |
| A storage made private | Narrows |
| A deleted storage | Removes (every file in it goes with it, and the plan says how many) |
Settings
settings.yaml, at the root of the bundle, holds the app's brand and which of your connections the
app may use. List "settings" in manage to use it.
# settings.yaml
branding:
displayName: Cordell Group
colors:
light: { primary: '#1F4E79', accent: '#F2A900' }
footer:
company: Cordell Group LLC
supportEmail: help@cordell.example
email:
- alias: billing
connection: Billing SMTP # an email connection's label in the console
default: true
tools:
- alias: crm
connection: Salesforce # an external tool connection's label in the console
tools: [lookup_account, update_account]| Key | Meaning |
|---|---|
branding | The brand on the app's email: displayName, colors (light and dark, each primary, accent, background), footer (company, address, supportEmail, website, social) and email (replyTo, footerNote). |
email | The email connections the app may send through, each under an alias, with at most one default. |
tools | The external tool connections the app may call, each under an alias, with the tools it may use. |
A connection is named by its label, so the same file works on every app you deploy it to, each using its own connection of that name. The label must name exactly one of your connections, and every tool must be one you have reviewed and enabled on it, or the plan fails before anything changes. A bundle never holds the connection's address or credentials.
The brand's images (logo, dark logo, icon) are files you upload in the console, so they stay the console's: a deployment keeps them as they are, and changing only an image in the console is allowed while the rest of the brand is managed by the repository.
| Change | Group |
|---|---|
| A connection the app may now use, a tool it may now call, an alias pointed at another connection | Widens authority |
| A new brand, a new default email connection | Changes |
| A connection or tool the app may no longer use | Narrows |
Removing settings.yaml, or a grant from it, revokes only what a deployment granted. A grant made in
the console that the file never named is left alone, and a brand the file no longer declares goes
back to the console unchanged.
Environments
One repository can deploy several apps, such as staging and production, each connected to its own
branch. Each app has an environment name, set by the app owner on its Deployments page, and
environments/<name>.yaml holds what differs for that environment. A deployment merges it over the
section files before checking them, so the result meets the same rules as any file.
# environments/staging.yaml
settings:
email:
- alias: billing
connection: Staging SMTP
jobs:
nightly_dunning:
schedule: { cron: '0 * * * *' }
agents:
refunder:
llm: { connection: Staging OpenAI }An environment changes where the app connects and how much it does, never what the app is or who may do what. These are the fields an environment may set:
| Section | Fields |
|---|---|
settings | branding, email, tools |
jobs | schedule, payload, timeoutSeconds |
agents | llm, effort |
storages | maxFileBytes, allowedMimeTypes |
Tables, roles, policies, custom actions and handlers are the same in every environment, and so are a job's or an agent's roles, an agent's auto-approval and a storage's visibility. What was reviewed and run in staging is what production runs, and an approval means the same thing everywhere.
Mappings merge key by key; a list or a value replaces what the file says, and null removes a key.
An environment file changes objects the bundle declares; it never adds one. Every environment file is
checked on every deployment, whichever one applies, so a mistake in one fails them all. An app with
no environment, or one with no file, deploys the bundle as written.
Handlers
Each handler folder, with its shared folders and the bundle's Deno config, becomes one version of the handler. A deployment publishes a new version only when the files differ from what is live, so deploying an unchanged bundle changes nothing.
Every check the console runs on handler code runs on each handler in a bundle, before anything is
applied: SQL passed to sdk.query() must be a string literal, external tool calls must be written
literally and name a tool the app has, and none may run inside a transaction. A handler that fails
one fails the deployment's plan.
Dependencies
A deno.json at the bundle root (or in functionsRoot) is the handlers' import map. When it names
a remote dependency (npm:, jsr: or https:), a deno.lock must be committed beside it, or the
deployment is refused: a rollback has to restore the same dependency graph as the code.
Limits
A bundle holds at most 2,000 files, none larger than 1 MB, and 16 MB in total. A bundle declares at most 500 handlers.
What a bundle never holds
Data (rows, users, files, runs, logs), secrets (keys, tokens, passwords) and who holds which role stay in the app. A bundle that needs a connection names it by its alias, never by its value.