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.mjs

A 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 }
  }
}
KeyDefaultMeaning
formatVersionrequiredThe 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:

KeyMeaning
entrypointThis handler's entrypoint file, instead of the default.
entryActionThe action the handler backs. Defaults to <name>:execute.
kindendpoint (default), table_action, worker (job-only), sdk or other.
descriptionOne line saying what the handler does, shown wherever a handler is picked. At most 500 characters.
sharedThis handler's shared folders, replacing the list above for this handler only.
excludetrue 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
KeyDefaultMeaning
descriptionWhat the table holds.
scopeModesharedscoped: each row belongs to one of the app's organizations. shared: every organization sees every row.
deleteStrategyhardsoft keeps deleted rows, marked deleted.
locked[]REST operations closed on the table: create, read, read_list, update, delete.
allowWritetrueWhether the console's data explorer may edit rows.
columnsrequiredThe 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:

KeyDefaultMeaning
namerequiredLowercase letters, digits and underscores.
typerequiredThe MySQL type: varchar(190), char(2), int, bigint unsigned, decimal(12,2), boolean, text, json, datetime, date, enum('a','b').
nullablefalseWhether the column may be empty.
defaultThe value a new row gets.
descriptionWhat the column means.
redactfalseNever return the column's values in reads.
renamedFromThe 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:

ChangeGroup
A new table, column or indexAdds
A wider type (a longer varchar, int to bigint, more enum values), a rename, a new order, a new foreign keyChanges
A column that is no longer redacted, a table that becomes shared, a REST operation that opensWidens authority
A type that may not keep every value, a column made required while some rows are empty, a deleted column, a deleted tableRemoves (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
KeyMeaning
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:

ChangeGroup
A new policy, a new roleAdds
A role that carries a policy, a policy that allows another action or changes a condition, a policy turned back onWidens 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 carriesNarrows
A deleted roleRemoves (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
KeyMeaning
method, pathThe route, on its table: together they name the action.
descriptionWhat the action does.
handlerThe handler that runs it, by name: one in the bundle or already in the app.
allowedActions, denyActionsWhat the handler may do while it runs for this action (its envelope).
schemaThe action's input fields, in the console's field format.
agentToolOffers the action to agents: name, description and risk (low, moderate, high).

How each change is classified:

ChangeGroup
A new action that delegates anything, an envelope that allows more, an action offered to agentsWidens authority (the approver must hold what it delegates)
A new action that delegates nothingAdds
A new handler, schema or descriptionChanges
An envelope that allows less, an action removed or no longer offered to agentsNarrows

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
KeyDefaultMeaning
namerequiredThe agent's name.
descriptionIts instructions.
effortshortbrief, short, moderate, long or max.
roles[]The roles it holds, by key (every agent also holds the built-in agent role).
llmthe platform's AIconnection: the label of one of your AI connections; model: optional.
autoApprovefalseApprove its own steps instead of pausing for a person.
autoApproveBudgetfalseGo 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:

ChangeGroup
A role the agent gains, approving its own steps, going past its budget without askingWidens authority (the approver must hold what the role allows)
A new agent that holds nothingAdds
A new name, instructions, effort or AI connectionChanges
A role it loses, pausing for a person againNarrows
A deleted agentRemoves (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]
KeyDefaultMeaning
namerequiredThe job's name.
descriptionWhat it is for.
handlerrequiredThe handler it runs. It must exist in the app or in the same bundle.
schedulerequiredevery (seconds) or cron (minute hour day month weekday), never both, with an optional timezone.
payload{}What the handler receives.
timeoutSeconds300How 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.

ChangeGroup
A role the job gainsWidens authority (the approver must hold what the role allows)
A new jobAdds
A new name, handler, schedule, payload or timeoutChanges
A role it losesNarrows
A deleted jobRemoves

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]
KeyDefaultMeaning
namerequiredThe storage's name.
visibilityprivatepublic lets anyone with a file's link read it.
maxFileBytesthe plan's limitThe largest file it accepts.
allowedMimeTypesany typeThe 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.

ChangeGroup
A storage made public, or a new public storageWidens authority
A new private storageAdds
A new name, size limit or file typesChanges
A storage made privateNarrows
A deleted storageRemoves (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]
KeyMeaning
brandingThe 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).
emailThe email connections the app may send through, each under an alias, with at most one default.
toolsThe 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.

ChangeGroup
A connection the app may now use, a tool it may now call, an alias pointed at another connectionWidens authority
A new brand, a new default email connectionChanges
A connection or tool the app may no longer useNarrows

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:

SectionFields
settingsbranding, email, tools
jobsschedule, payload, timeoutSeconds
agentsllm, effort
storagesmaxFileBytes, 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.

On this page