App manifest
The permission contract of an app version. Every field, generated from the schema the API and the gate validate with, and an example with the sentences a person reads before approving.
Every app version carries a manifest. The builder proposes it, a person approves it, and the gate enforces it: anything not declared here is denied. The manifest of a published version never changes; wider access is a new version.
You rarely write one by hand. A plan from the Build screen, plan_app_from_prompt or qaldy build --plan contains one, and shows it as sentences. This page is for reading a plan closely, for reviewing what an agent proposes, and for building tools around the API.
An example#
{
"name": "Low-stock alert",
"description": "Tells the roastery chat in Telegram when a coffee bean stock drops below 5 kg.",
"archetype": "radiotower",
"category": "messaging",
"permissions": {
"data": [
{
"table": "stock",
"access": "read"
},
{
"table": "alerts_sent",
"access": "write"
}
],
"connectors": [
{
"id": "telegram",
"actions": [
"sendMessage"
],
"scope": "#roastery"
}
],
"egress": [],
"ai": null
},
"triggers": [
{
"type": "schedule",
"cron": "0 8 * * 1-5"
}
],
"sharing": {
"default": "private"
},
"roi": {
"manualMinutesPerRun": 10
}
}The plan shows this manifest to the person who approves it as these sentences:
- Can read stock.
- Can change alerts_sent.
- Can send Telegram messages to #roastery through Telegram.
- Does not use AI models.
- Can't reach anything else on the internet.
Nothing here reaches the internet: egress is empty and ai is null. Each connector action is named, so the app can send a message to that one chat and cannot do anything else through Telegram. The app can read the stock table and write alerts_sent, and no other table.
Every field#
The table is generated from the schema the API, the builder and the gate all parse with, so it cannot drift from what is enforced.
| Field | Type | Required | Notes |
|---|---|---|---|
name | string | yes | 2 to 60 characters |
description | string | yes | 4 to 280 characters |
archetype | "kiosk" · "tower" · "warehouse" · "workshop" · "bank" · "townhall" · "clocktower" · "printhouse" · … | yes | |
category | "interface" · "data" · "messaging" · "money" · "ai" · "time" · "people" · "growth" | yes | |
permissions | object | yes | |
permissions.data | object[] | yes | |
permissions.data[].table | string | yes | 1 to 48 characters |
permissions.data[].access | "read" · "write" | yes | |
permissions.connectors | object[] | yes | |
permissions.connectors[].id | "shop" · "stripe" · "gmail" · "telegram" · "sheets" · "slack" · "kaspi" · "1c" · … | yes | |
permissions.connectors[].actions | string[] | yes | |
permissions.connectors[].scope | string | up to 80 characters | |
permissions.egress | object[] | yes | |
permissions.egress[].domain | string | yes | 3 to 253 characters |
permissions.egress[].reason | string | yes | 8 to 200 characters |
permissions.ai | object or null | yes | |
triggers | object[] | yes | |
sharing | object | yes | |
sharing.default | "private" · "team" · "workspace" | yes | |
roi | object | yes | |
roi.manualMinutesPerRun | number | yes | 0 to 1440 |
How the permissions are enforced#
| Field | Enforced by |
|---|---|
permissions.data | The runtime host builds the app's data binding from it. A table not listed does not exist for the app, and write is separate from read. |
permissions.connectors | The gate matches each call to {connector, action} in the version's manifest. Anything else is 403 egress_blocked (action_not_declared). |
permissions.egress | The gate allows exactly these hosts, plus the connectors' own hosts: HTTPS, port 443, an exact name. Each needs a reason a person can read. Wildcards and IP addresses are refused. |
permissions.ai | The gate allows model calls only for the declared models, within the monthly budget. An undeclared model is refused. |
triggers | The platform starts a run on a schedule, a webhook, a form, a button press or an event. |
sharing.default | Who can open a new app at first; sharing then works as described here. |
A manifest is checked twice: the builder's static checks against the code (declared egress must equal used egress, see Approvals and the Trust Score), and the gate at run time against the actual calls.