Agent instructions

OpenCloud build skill

---
name: opencloud-build-apps
description: Build, validate, deploy, verify, operate, and troubleshoot static web applications on OpenCloud through hosted MCP or the app-scoped CLI, with its versioned JavaScript SDK, bounded PostgreSQL data, central Auth, managed Files, native Web Push, private Realtime, Functions, background jobs, app email, cron, safe telemetry, and backups. Use for live OpenCloud app work or for an honestly offline-validated bundle when no online execution path is available.
---

# Build apps on OpenCloud

Use this skill as the execution policy and <https://docs.opencloud.ai> as the
interface reference. When a usable terminal is available, use the public,
versioned OpenCloud CLI (source: <https://github.com/opencloud-ai/cli>) even if
hosted OpenCloud MCP tools are also exposed. Reserve MCP for surfaces that cannot run
the CLI or call the API, such as mobile-only ChatGPT or Claude sessions. MCP
and the CLI use the same control-plane contract, server-side drafts, validator,
deployment executor, and verification operation. Do not inspect platform
internals to infer undocumented behavior.

## Quick start in a terminal

The usual first build with the CLI, in order. Each step is explained below.
In an exact-app owner Agent session, the app already exists: skip steps 2
and 3 and use that session.

1. Install the pinned CLI with
   `curl -fsSL https://docs.opencloud.ai/install.sh | bash`, apply any
   `export PATH=…` line it prints, and check it with `opencloud --version`.
   "Connect with the CLI" has the details.
2. Run `opencloud login` and let the person approve it in their browser.
3. Create the app once, with a key you reuse if you retry, so a retry can't
   make a second app:
   `opencloud app create --name "$PROJECT_NAME" --visibility private --idempotency-key "$CREATE_KEY"`.
   Keys are 8 to 128 letters, digits, dots, underscores, colons or hyphens,
   such as `create-2026-10-27-reading-log`.
4. In a new, empty app folder, run `opencloud app connect "$APP_ID"` and
   `opencloud init "$APP_DIR" --app-id "$APP_ID"`, which starts a schema-3 app.
   Run every later app command from this folder: the connection lives there,
   and in another folder the CLI asks you to log in again.
5. Build in small steps: `opencloud validate "$APP_DIR"`, then
   `opencloud app dev start "$APP_DIR"` once and `opencloud app dev sync "$APP_DIR"`
   after each change. Show the person the returned `browserPreviewUrl`.
6. Write `tests/opencloud.e2e.js`, run `opencloud app dev verify`, and once
   the person agrees, `opencloud app dev promote`, each with its own
   `--idempotency-key`.
7. Check the live app signed out with `opencloud app request "$APP_ID" /`.
8. To let other people in, give the person the app's Sharing page:
   `sharingUrl` from `opencloud app get`. The CLI can't invite anyone.

Keep the person's own files, such as spreadsheets and exports, outside the app
folder, or list them in `.gitignore`. Everything else in the folder is uploaded
with the app, and its admins can download it with the app's source.

Test in the preview. A CLI login can't run owner-only actions, such as
deleting rows in the live app or running a schedule now; the owner does those
in the app or its dashboard. So give the owner a way to remove anything the
app stores before you add a test entry to the live app.

## Select CLI, MCP, or offline mode

Inspect the execution surface first. When a terminal is available:

1. Install and verify the pinned CLI release below.
2. If OpenCloud explicitly supplied an exact-app owner Agent session and its
   assigned app ID, use that session directly. Run `opencloud doctor` and
   `opencloud app get "$APP_ID"`; do not replace it with account login, create
   another app, or connect a different source directory.
3. Otherwise run `opencloud auth status`. Reuse a healthy account login or run
   `opencloud login` and let the user approve it in their browser. Both name
   the signed-in person in `user`: tell the user which account you are using,
   and run `opencloud login --force` when they want a different one. Run
   `opencloud app list`, select or create the requested app, and from its
   source directory run `opencloud app connect "$APP_ID"`.
4. Use the CLI for drafts, isolated development, deployment, verification, and
   operations. Do not switch to MCP merely because both interfaces are
   available.

When no usable terminal or direct API path exists and OpenCloud MCP tools are
present:

1. Use `list_apps`, or `get_app` when the app ID is already known. Protected
   calls trigger normal OAuth sign-in when needed; no session-status tool is
   required. An empty app list is valid.
2. Use `connect_opencloud` only to explicitly reconnect an existing account.
   For first-project registration, continue with `start_onboarding`.
3. For first registration, ask once for the email if necessary and call
   `start_onboarding` with the agreed project name and visibility.
4. A new email receives a retained request with `awaiting_email_verification`
   and no app or credential.
   Building needs an organisation: after confirming their email, an owner
   without one creates or joins one through `launchUrl`, and
   `complete_onboarding` returns `organisation_setup_required` until then. An
   account can belong to at most one company. Never infer membership from an
   email domain or create a replacement company for a removed or suspended
   account.
5. Give the owner the returned `launchUrl` as the primary link while email
   confirmation or deployment is pending. The confirmation message names the
   project and asks the owner to confirm the request. The retained request
   opens the project after deployment.
6. Existing emails also approve the emailed request. After confirmation,
   call `complete_onboarding` with the same onboarding ID. Its private
   completion token remains inside the MCP session. While confirmation is
   pending, wait for the owner instead of starting another request.
   - If `verification.emailSent` is false, the email could not be sent yet:
     tell the owner and call `start_onboarding` again shortly with the same
     details.
   - `onboarding_rate_limited` means the address already received several
     confirmation emails; use the latest one, or `connect_opencloud` for an
     existing account, instead of retrying.
   - If the MCP session ends, or `complete_onboarding` returns `ready` with a
     null credential, after the owner confirmed, call `connect_opencloud`
     instead of starting another request.
   - A `plan_limit_reached` error names the limit and, where the installation
     bills, an `upgradeUrl`. Give both to the owner and continue only after
     they act. Inspect account limits in the owner's dashboard when needed.
7. Resolve the assigned app from the onboarding result or call `get_app`.
   Before selecting capabilities, make one monitoring decision: determine
   whether the product has an important failure that OpenCloud's built-in
   app, deployment, operation, cron, background-job, HTTP-error-rate, and
   telemetry-freshness signals cannot express. If it does, include
   `telemetry`; otherwise do not add custom monitoring merely because it is
   available. This is an agent decision, not a user approval step.
   Call `get_app_starter` with every capability implied by the request: `auth`,
   `data`, `files`, `functions`, `jobs`, `ai`, `email`, `notifications`, `realtime`, `telemetry`,
   `cron`, and/or `integrations`.
   Implement every returned `resolvedCapabilities` and `capabilityChecklist`
   entry. Use its current minimal manifest, SDK pattern, external E2E
   specification, and build invariants before authoring the first draft; call
   it again if the product scope grows.
8. Use source-draft, dev-session, verification, and promotion tools for all
   live work. Do not require the CLI as a second preflight.

## Understand how hosted MCP works

OpenCloud MCP is a stateful Streamable HTTP protocol, not a remote shell:

- Use `/build` on the canonical `mcp.<domain>` host for a native app-building agent. It exposes only onboarding,
  assigned-app inspection, draft, isolated dev, promotion, deployment
  evidence, and verification tools. Use the host root `/` for the full lifecycle on
  supported no-terminal connector surfaces. Terminal-enabled Codex, Claude
  Code, and similar agents use the CLI instead.
- `/mcp` and `/mcp/build` remain compatibility aliases; configure new clients
  with the canonical host root or `/build` endpoint.
- Initialization returns an opaque `mcp-session-id`. Reuse it on subsequent
  requests and deletion. A session belongs to the surface that created it;
  do not move one session between the full root and `/build` surfaces.
- The server retains only bounded onboarding state and a provisional grant
  created in that session. OAuth bearer credentials remain request-scoped.
  Neither credential is tool output, source content, or something to request
  from the user.
- Tools are authoritative actions. `opencloud-skill` is a supplemental MCP
  resource; some clients do not read resources automatically, so server
  instructions and `get_app_starter` carry the essential workflow too.
- Draft files live on OpenCloud. `expectedRevision` prevents overwriting a
  newer draft, and each `baseSha256` prevents overwriting a file that changed
  since it was read. On conflict, call `get_draft` plus `list_files` or
  `read_files` and rebase the intended change; never guess a revision/hash.
- A draft cloned from the live release (the `create_draft` default) holds that
  release's complete source and may replace only that release. If someone
  releases in between, `start_dev_session` and `deploy_draft` fail with
  `ACTIVE_DEPLOYMENT_CHANGED`; never deploy over a release you have not seen.
- `validate_draft` builds one immutable artifact from one source revision.
  Read `sourceManifest` and `sourceFiles` as author inputs, `artifactFiles` as
  packaged output, and every diagnostic `path`/`suggestedFix` before editing.
- A dev session materializes only a validated revision in an isolated
  database. Each later edit requires validation and `apply_dev_revision`.
  Synthetic data and explicit Function invocations produce evidence without
  touching production.
- `verify_dev_session` runs strict Chromium TLS/runtime checks plus the
  immutable `tests/opencloud.e2e.js` specification. Its receipt binds the
  exact revision, artifact, migration digest, and E2E source hash; any later
  edit invalidates the receipt. External tests run in independent child
  sandboxes with fresh migration-replayed data, Storage, Functions, and users.
  Up to five tests run concurrently by default; callers may request 1 through
  10 without changing the test artifact.
- Promotion and production verification return durable operations. Poll the
  matching `operation.id` with `get_operation` to a terminal state, then poll
  the matching verification. A queued operation or preview is not done.

Recover from failures by preserving evidence and changing the smallest
boundary:

| Failure                              | Correct recovery                                                                                 |
| ------------------------------------ | ------------------------------------------------------------------------------------------------ |
| draft revision or file hash conflict | Re-read draft/files and reapply only the intended change.                                        |
| `ACTIVE_DEPLOYMENT_CHANGED`          | Someone released after your copy; start from the latest release and reapply only your change.    |
| manifest diagnostic                  | Fix the reported source path; use `get_app_starter` when the manifest or SDK pattern is missing. |
| development migration failure        | Fix the named migration file/SQLSTATE, validate a new revision, and apply it.                    |
| Chromium certificate failure         | Treat it as an environment TLS failure; never disable certificate validation.                    |
| expired/stale receipt                | Reapply the exact current revision, repeat dev checks, and obtain a new receipt.                 |
| durable operation failure            | Report the terminal failure and diagnostic; do not promote or claim completion.                  |

Pasting a prompt cannot install an MCP connector. If tools are absent, state
that OpenCloud must be enabled on a supported agent surface.
Claude Code and Cowork can install the OpenCloud plugin; Claude.ai, Claude
Desktop, and Claude mobile can use the hosted connector. ChatGPT plugins and
arbitrary MCP tools are not currently available directly in ChatGPT mobile
chats; mobile can steer an already configured desktop task through ChatGPT
Remote.

For every terminal workflow, establish CLI execution capability before
credentials.

The pinned CLI release for this skill is `v3.18.0` and requires Node.js 22 and
npm 10 or newer. The installer downloads that exact release, verifies its
published checksum, and installs the `opencloud` command for the current user:

```bash
curl -fsSL https://docs.opencloud.ai/install.sh | bash
test "$(opencloud --cli-version)" = "3.18.0"
```

CLI 3.18.0 requires explicit idempotency keys for app-owner Agent mutations
and supports schema-3 manifest `routes`, SDK 2.3.0 Function routes, and `app
domain get|add|check|remove`. Keep custom domains outside the manifest, reuse
an idempotency key only for the same intended mutation, and follow the returned
DNS and readiness observations before treating a domain as connected.

CLI 3.18.0 uploads each app's whole source tree (honouring `.gitignore`) so
every release keeps its source, which the app's admins can download. It names
the uploaded files that aren't part of the app (`sourceOnlyFiles`). Keep the
person's own data files outside the app folder, or in `.gitignore`, and read
them from there. The CLI also records the release a copy is based on in
`.opencloud/base.json`. `deploy` and `app dev start` send it, so the
platform refuses with `ACTIVE_DEPLOYMENT_CHANGED` when someone has released
since. Then run `opencloud pull <directory>` to bring that release in. When
it would overwrite your edits, pull it into a separate directory with `--app`
and `--deployment`, merge it, and record the merge with
`opencloud pull <directory> --deployment <id> --merged`. Use `--force` only
when the person asks to replace the newer release. Pulling into a new
directory with `--app <id>` also starts a copy of an existing app.

If the installer reports that its user-level binary directory is not on
`PATH`, apply the `export` command it prints before running the version check.

Do this CLI preflight before starting CLI login. If the
environment cannot execute a shell, Node.js 22/npm, HTTPS downloads, or the
verified CLI, it cannot deploy from the public CLI. If MCP is also unavailable,
work honestly offline.

Work offline only when both MCP and the verified CLI are unusable. Build and
locally validate a complete source tree, but do not claim live deployment or
verification.

## Connect with the CLI

An exact-app owner Agent session is already connected. OpenCloud supplies it
to AI SDK Codex harness v3 through `OPENCLOUD_SESSION_FILE` together with the
assigned app identity. Never open or print that file, run browser login, create
an app, or exchange the session for another credential. The session delegates
human-owner-equivalent authority for the exact assigned app: the Agent may use
every public CLI app operation, including deployment, production mutation,
rollback, restore, secret and access changes, archive, and deletion. It does
not need an additional confirmation merely because an operation is destructive
or affects production. Honor explicit user constraints such as “do not deploy”
and use ordinary product judgment, validation, and recovery.

That session cannot create another app, enumerate or operate another app,
change account settings, authorize or reveal account-owned provider
credentials, administer the platform, or access the host or shared
infrastructure. Do not infer this authority from merely running as an Agent.
Legacy AI SDK harness v1/v2 sessions and ordinary human-directed CLI sessions
retain their existing authority and approval rules.

Every nested deployment, backup, operation, credential, File, binding, grant,
or other resource is resolved back to its parent app before authorization. A
foreign ID and a nonexistent ID produce the same non-enumerating denial. Do not
probe either and never retry a boundary denial with account or platform
credentials.

For an exact-app owner Agent, “every app operation” includes:

- app inspection and supported lifecycle/configuration changes, including
  visibility, restart, archive, unarchive, and final deletion;
- draft and source-file management, isolated development, captured effects,
  validation, verification, deployment, active-artifact inspection, rollback,
  and inactive-deployment cleanup;
- production table and managed-File administration, including destructive
  row and File mutations;
- secret metadata, generation, secure set/replace, entry links, and deletion;
- backup creation, schedule, inspection, and restore;
- production Function invocation plus cron, job, queue, and app-email
  operations through their supported typed surfaces;
- logs, metrics, usage, visitors, alerts, Agent Feed, verification, and
  durable-operation evidence;
- app members, access grants, app credentials, and app-scoped access tokens;
  and
- listing, binding, configuring, using, and unbinding app integrations through
  typed app APIs.

Some app behavior intentionally remains reachable only through app code. For
example, enqueue through a declared Function rather than inventing an owner
queue bypass, and send email or execute a provider integration through its
typed Function client. Account connection creation/consent, `calling_user`
bindings, raw provider credentials, and unsupported platform features such as
ownership transfer are not app-owner CLI operations.

The complete owner-parity command tree is documented in the CLI reference for
the pinned public CLI `v3.18.0`. Confirm the exact version and command help
before using owner operations. Do not invent a missing command or bypass the
public CLI with a raw control-plane request; report an absent surface as a
product limitation.

For ordinary CLI use, account login is per OS user and app connection is per
source workspace, the local directory bound to one app:

```bash
opencloud auth status
opencloud login
opencloud app list

# Only when the requested app does not already exist. Reuse the same key if
# you retry, so a retry can't create a second app:
CREATE_KEY="create-$(date +%Y%m%d)-first-app"
opencloud app create --name "$PROJECT_NAME" --visibility private \
  --idempotency-key "$CREATE_KEY"

opencloud app connect "$APP_ID"
opencloud doctor
```

- `opencloud login` creates a short-lived browser approval request, prints and
  opens its HTTPS URL, and polls while the user signs in with a one-time email
  link or configured password and explicitly approves the CLI. It does not
  start a localhost callback or ask the user to paste a code, email link,
  cookie, or token. Use `--no-browser` when the terminal cannot open a browser;
  give the user the printed approval URL.
- `opencloud whoami` names the person the CLI acts as (`user`), how its
  credential was obtained (`credential.kind`), and where the CLI found it
  (`credential.source`). An app credential acts for the person who created
  it, and an exact-app owner Agent session for the person who delegated it.
- Approval returns a 15-minute account access token and a rotating 30-day
  refresh token. The account token may list, read, and create apps and connect
  a workspace; it cannot deploy, configure an app, read secrets, or perform
  owner-only actions. The one exception is the app's custom domain, which the
  person who created the app may add, check and remove with their own account
  token while they can still administer it.
- Secrets are stored under the OS credential service `ai.opencloud.cli` when a
  keyring is available. Headless environments fall back to a per-user,
  mode-`0600` file under `$XDG_CONFIG_HOME/opencloud/credentials` (normally
  `~/.config/opencloud/credentials`) on Linux,
  `~/Library/Application Support/OpenCloud/credentials` on macOS, or
  `%APPDATA%\OpenCloud\credentials` on Windows. Never open, print, copy,
  upload, summarize, or commit these credentials.
- `opencloud app connect` writes only a non-secret, ignored
  `.opencloud/app.json` binding in the source tree. It stores a separate
  24-hour app-scoped credential in the same protected credential backend. The
  CLI renews that credential from the account login when it nears expiry.
- `opencloud logout` (or `opencloud auth logout`) revokes the refresh-token
  family and all workspace credentials issued from it, clears the local
  account credential, current workspace credential, and resolved onboarding
  session, and retains non-secret workspace bindings for later reconnection.
  `opencloud login --force` replaces an unusable stored login.
- `.opencloud/session.json` and `opencloud onboard` implement passwordless
  first-project onboarding. `OPENCLOUD_API_URL` selects a non-default
  installation, and `OPENCLOUD_TOKEN` supplies explicitly delegated authority.
  Never inspect an onboarding session file or ask for a copied credential.
- OpenCloud chooses the DNS-safe title-based slug and random suffix for new
  apps. Never ask the user to find an available domain.
- Treat `OPENCLOUD_EDGE_URL` only as an optional CLI transport adapter. Preserve
  and report the canonical HTTPS app URL.
- Never invent IDs, URLs, credentials, secrets, operations, or results.
- Never print or commit tokens, passwords, cookies, `.env` contents, secret
  values, or brokered access tokens.

Run the CLI from any directory:

```bash
opencloud <command>
```

Workspace and legacy-session discovery search parent directories, so commands
work from nested source folders. `opencloud doctor` prints a redacted view of
the CLI version, credential backend, app identity, endpoint reachability, and
deployed platform version. Pass absolute app-directory paths when more than one
app is present.

## Use structured results and recover durable operations

The public CLI emits one stable machine-readable JSON result for every
successful command. Read fields from that document instead of scraping prose.
The owner-parity release also makes every failure structured, redacted JSON
with a non-zero exit status. Retain the error code, request ID, retryability,
operation ID, and bounded diagnostic; never retain credentials or secret
values.

When OpenCloud itself fails rather than the request (an internal error, a 5xx
answer, a tool that answers wrongly), the platform records it for the OpenCloud
team. Through MCP, call `report_problem` when the user asks to report a problem
or agrees to send one: describe what they were doing and what happened, include
the error text and request IDs, and give the user the returned reference.

After the owner-parity release is pinned, `app credential-create` requires a
new `--token-file` path, writes the one-time token there at mode `0600`, and
returns only redacted metadata plus the path in JSON. Never open, echo, trace,
or report that file. Its idempotency key is required. If an ambiguous response
leaves no file, repeat the exact request and key to replay the same credential;
never overwrite an existing token file. The exact-app owner session normally
needs no second app credential.

For every mutation, use one stable idempotency key for one intended effect and
reuse it after a timeout, disconnect, or lost response. An exact-app owner
Agent must pass it with every mutating command's `--idempotency-key` option;
the CLI rejects a missing key before creating a journal entry or request. A new
attempt with materially changed input is a new intended effect and gets a new
key. An artifact digest may identify input but is not sufficient by itself to
identify the intent. Never create a new key merely because the first response
was ambiguous. On `MUTATION_JOURNAL_AUTHORITY_MISMATCH`, do not delete journal
state or change keys to bypass recovery; inspect the exact operation and
current app state first.

Operation-starting commands follow their durable operation by default with a
bounded timeout. Use `--no-follow` only when deliberately detaching, and retain
the returned operation ID before doing other work. Recover without repeating
the mutation:

```bash
opencloud operation get "$OPERATION_ID" --follow
opencloud operation list "$APP_ID"
```

`operation get` accepts bounded interval and timeout options. A local wait
timeout says only that the terminal result was not observed; it does not mean
the mutation failed. Re-read the operation. Terminal failure or cancellation
exits non-zero and preserves typed diagnostics. After repairing input, start a
new intended effect with a new idempotency key. App deletion is special only in
lifecycle: the successful terminal result is returned before the app session
is revoked, and that session may read only its exact deletion receipt for a
bounded time afterward.

## Read only the docs you need

Read these first:

1. <https://docs.opencloud.ai/getting-started/>
2. <https://docs.opencloud.ai/getting-started/agents>
3. <https://docs.opencloud.ai/sdk/javascript/>
4. <https://docs.opencloud.ai/reference/manifest>
5. <https://docs.opencloud.ai/guides/development>
6. <https://docs.opencloud.ai/reference/verification>
7. <https://docs.opencloud.ai/guides/functions-cron>

Then read the capability page before implementing Auth, database, managed Files,
Web Push, Realtime, Functions/background jobs/cron, telemetry, or verification. Use
<https://docs.opencloud.ai/llms.txt> as the compact documentation index.

## Discover the assigned app

With MCP, use `list_apps` and `get_app`. Treat `get_app` as authoritative for
`appUrl`, `authUrl`, and `apiUrl`. Then call `get_app_starter` with the
assigned app ID and complete capability list. Treat
its resolved checklist as required. Use its files as a current scaffold, not
permission to overwrite intentional existing source.

With the CLI:

```bash
opencloud app list
opencloud app get "$APP_ID"
opencloud app origin "$APP_ID"
```

Treat `app get` as authoritative for `appUrl`, `authUrl`, and `apiUrl`.
An exact-app owner Agent can inspect and manage credentials, restore backups,
and delete its assigned app because its delegated role explicitly grants full
owner authority for that app. It cannot create another app or use any supplied
ID to cross the assigned-app boundary. Narrow app-scoped and legacy Agent
credentials retain their existing scope and action restrictions.
Provisional account grants can create multiple new apps during their 24-hour
verification window:

With MCP, call `create_app`. With the CLI:

```bash
opencloud app create \
  --name "Another project" \
  --visibility private \
  --idempotency-key "create-another-project"
```

Use a private app when its UI or data requires a user. Let the edge redirect to
the central OpenCloud access page; do not build a second sign-in form.
Use `--visibility public` (MCP `visibility: "public"`) for an app anyone can
open, including one that runs its own sign-in. The created app's `visibility`
is the stored value. Use `app configure --visibility` to change it later.

People get into an app from its Sharing page in the dashboard. `get_app` and
`opencloud app get` return its address as `sharingUrl`: give the owner that
link.
On this installation the owner shares it with people in their organisation.
Data policies still decide what each person sees: use `auth.uid()` for a
person's own rows and `auth.is_app_admin()` for the app's admins. A policy
that lets every signed-in user read also lets everyone read once the app is
public. When some people should change some of the data but not the app
itself, such as coaches adding results while captains only read, invite them
to use the app and give them a role inside it: keep the role in a table and
check it in your policies. App Admin would also let them change and publish
the app.

An app for people without an account, such as customers, parents, guests or
event visitors, must be public. Keep its owner and staff screens for app
admins with `auth.is_app_admin()` policies. Signed-out visitors can read data
that a policy deliberately makes public, but they cannot write. Accept their
submissions, such as sign-ups, orders and bookings, in an `access: public`
Function that validates the input and calls `jobs.enqueue`, and save them in an
`access: system` queue consumer under `auth.is_system()` policies for both
insert and select. An anonymous insert policy does not work: anonymous writes
are refused, and `create` must read the new row back. Confirm a submission as
soon as the Function accepts it; the queue saves it moments later, so never
wait for the saved row or report a failure because it is not there yet. Before
you publish, test the visitor's journey in `tests/opencloud.e2e.js` with
`anonymousPage`, which opens the development app signed out. Once it is live,
check the app as a signed-out visitor with `opencloud app request <app-id> /`,
which must return the page rather than a redirect to sign-in.

## Change an existing app safely

For a change request, inspect and validate the existing bundle before editing.
Do not run `init`, replace the app ID, recreate the app, or rewrite an applied
migration. Preserve current behavior outside the request, append ordered
migrations for schema changes, update product tests and the required external
E2E specification, then deploy. OpenCloud assigns the new release name during
promotion; do not edit a legacy schema-2 top-level `version` merely to make an
update unique. Confirm the active
deployment before and after the change and leave the previous release available
for an authorized human owner or exact-app owner Agent to roll back. Preserve
`runtime.sdk.version`
unless the request explicitly includes an SDK upgrade; an upgrade is a new
release and must pass the complete product and browser gates.

## Make an immediate artifact checkpoint

Create a real manifest and non-empty frontend in the first coherent file batch:

With MCP, call capability-aware `get_app_starter`, then `create_draft` and
inspect its files.
Use `apply_file_changes` with the current draft revision and per-file hashes.
Include `opencloud.yaml` plus a non-empty configured frontend in the first
coherent batch, and call `validate_draft`. Read source/artifact file lists and
diagnostic fixes from the result before continuing. Grow the app in small coherent revisions;
use `apply_dev_revision` after each later validation.

With the CLI:

```bash
opencloud init "$APP_DIR" \
  --app-id "$APP_ID"

opencloud artifact-check "$APP_DIR" \
  --expect-app-id "$APP_ID" \
  --max-files 4

opencloud validate "$APP_DIR"
```

Grow the product in small coherent batches. Re-run the checker after changing
manifest-reachable paths and validate after each runtime boundary.

## Author the deterministic bundle

Use this layout:

```text
app/
├── opencloud.yaml
├── frontend/
├── migrations/
├── functions/
└── AGENT_REPORT.md
```

Hosted MCP and schema-3-capable clients use this manifest pattern:

```yaml
schemaVersion: 3
appId: 6f9619ff-8b86-4e6e-a62a-889950f42d3e
frontend:
  directory: frontend
  spa: true
runtime:
  sdk:
    version: 2.2.0
files:
  access: user
  maxUploadBytes: 52428800
migrations:
  - id: 0001_create_items
    file: migrations/0001_create_items.sql
functions:
  - name: summarize
    entrypoint: functions/summarize/index.ts
    access: user
  - name: hourly-summary
    entrypoint: functions/hourly-summary/index.ts
    access: system
  - name: receive-support
    entrypoint: functions/receive-support/index.ts
    access: system
  - name: process-report
    entrypoint: functions/process-report/index.ts
    access: system
queues:
  - name: reports
    function: process-report
    concurrency: 2
    maxAttempts: 3
    retryDelaySeconds: 5
    retryBackoff: true
    timeoutSeconds: 120
email:
  addresses:
    - name: support
      displayName: Example Support
      function: receive-support
notifications:
  webPush: true
  icon: /icons/notification.png
cron:
  - name: hourly-summary
    schedule: "0 * * * *"
    function: hourly-summary
    enabled: true
health:
  path: /
secrets:
  INTERNAL_SIGNING_KEY: generated
  AI_API_KEY: required
  ORGANIZATION_LABEL: optional
integrations:
  calendar:
    provider: google-calendar
    account: calling_user
    cardinality: one
    capabilities:
      - calendar.events.read
  bank:
    provider: gocardless-bank-account-data
    account: calling_user
    cardinality: many
    capabilities:
      - bank.accounts.read
      - bank.balances.read
      - bank.transactions.read
observability:
  metrics:
    - name: items_created
      type: counter
      unit: items
      dimensions:
        actor_type:
          values: [member, admin]
    - name: overdue_items
      type: gauge
      unit: items
    - name: item_summarization_failures
      type: counter
      unit: failures
  alertRules:
    - id: overdue-items
      name: Overdue items detected
      metric: overdue_items
      aggregation: latest
      operator: gte
      threshold: 1
      window: 5m
      minimumSamples: 1
      severity: warning
    - id: item-summarization-failure
      name: Item summarization failure
      metric: item_summarization_failures
      aggregation: sum
      operator: gte
      threshold: 1
      window: 5m
      minimumSamples: 1
      severity: critical
```

Choose `files.access: app` only when authenticated members should share files;
keep the safer `user` default for per-user files. `files.maxUploadBytes` is an
integer from 1 byte through 100 MiB. Do not add a top-level release version to
schema 3; the publisher records it separately in the deployment database.
Pin the exact installed SDK version—never `latest` or a range. Keep migration
IDs ordered and append-only. Never write migration checksums; the CLI computes
them.

The pinned public CLI 3.18.0 defaults new versionless sources to schema 3 and
omits the top-level release version. Use `--version` only when intentionally
creating a legacy schema-2 source tree. Leave an existing schema-2 embedded
version stable across updates: the publisher ignores it for release naming and
assigns the database version. Immutable schema-2 artifacts remain readable.

Only the canonical manifest, configured frontend tree, declared migrations,
and declared Function source trees enter the archive. Inspect the exact file
list printed by `validate`.

A frontend that needs a build step (for example Vite) can let OpenCloud build
it: declare `frontend.build.script`, keep `package-lock.json` in the app
root, and upload source without the output directory. During validation
OpenCloud runs `npm ci --ignore-scripts` and `npm run <script>` in a
sandbox; that script must write `frontend.directory`. A failure is the
`FRONTEND_BUILD_FAILED` diagnostic with the build's last output, and
`FRONTEND_BUILD_BUSY` means validate again shortly. This works with MCP
drafts and with CLI 3.13.1 or later.

```yaml
frontend:
  directory: frontend/dist
  build:
    script: build
```

## Define app routes and custom domains

Use schema 3 for `routes`. Assets are regular files relative to
`frontend.directory`; the two aliases below serve the same file. Function
routes require an explicit upgrade to the paired SDK 2.3.0, a declared `user`
or `public` Function, and explicit methods. Preserve older SDK pins when no
route-driven upgrade is needed.

```yaml
runtime:
  sdk:
    version: 2.3.0
functions:
  - name: pixel
    entrypoint: functions/pixel/index.ts
    access: public
routes:
  - id: favicon
    path: /favicon.png
    asset: icons/app.png
    access: public
  - id: apple-icon
    path: /apple-icon.png
    asset: icons/app.png
    access: public
  - id: pixel
    path: /pixel.jpg
    function: pixel
    methods: [GET, HEAD]
```

Only exact literal asset aliases may use `access: public` in a private app.
The exception does not expose the source file URL, directory, HTML, config,
data, or Functions. Other routes inherit app admission. Asset methods are
GET/HEAD; Function methods may also include POST, PUT, PATCH, and DELETE.
Routes cannot replace `/`, `health.path` with a Function, or reserved service
namespaces. Unmatched paths retain the existing static/SPA behavior.

Use whole-segment `:id`, optional `{/literal}` or `{/:id}`, a final optional
extension as `/report{.:ext}` or `/files/:id{.:ext}`, and a terminal named
wildcard such as `/downloads/*parts`. Omitted optional params have no key;
wildcards are arrays of one or more segments. Do not use `:id?`, regex,
unnamed wildcards, nested optionals, or required mixed segments. Paths are
case-sensitive and trailing slashes matter. Specificity chooses the path
before method checking; ambiguous overlaps fail validation and a disallowed
method returns 405 without falling through. See
<https://docs.opencloud.ai/guides/routing> for limits and complete examples.

For `/pixel.jpg?param1=test123&tag=one&tag=two`, use the SDK 2.3.0 handler's
`http.query.get("param1")` and `http.query.getAll("tag")`; `has` and ordered
`entries` are also available. `http.params` supplies decoded path values.
Neither query nor params merges into schema-validated `input`. GET/HEAD input
is `{}`; other methods retain JSON input. `http` is `null` for ordinary
Function calls. Do not invent raw Request or cookie access; SDK 2.7.0 adds
read-only `headers` (below). HEAD runs the
handler and suppresses response bytes; keep GET/HEAD handlers free of write
side effects. Exercise the actual preview route, not only a named invocation.

Custom domains are owner app settings outside the manifest. One exact apex or
subdomain consumes the app's slot; `www` is separate. Use public CLI 3.18.0
`app domain get|add|check|remove`, App Settings → Domains, or full MCP
`get_app_domain`, `add_app_domain`, `check_app_domain`, and
`remove_app_domain`. CLI mutations use the exact-app journal and accept an
optional `--idempotency-key`; MCP mutations require an explicit
`idempotencyKey` of 8–200 characters. Reuse the key only with the same input
after an uncertain result. A confirmed owner is required, and `/build` does
not expose these tools. An app credential from `app connect` never changes a
domain, and an account login does so only for the person who created the app;
other app admins use App Settings or full MCP. CLI 3.18.0 sends the account
login for these commands when one exists; without it, it sends the
directory's app credential, which is refused.
Copy the provided TXT
ownership and traffic records; use apex ALIAS/ANAME/flattening or published
A/AAAA alternatives. Preserve MX/TXT and unrelated records. Cloudflare
proxying can remain enabled from the start; report actual check failures,
not speculative proxy warnings. Add optional ACME delegation only from the
provided record. Never ask for the owner's DNS credentials.

Only a connected domain becomes primary; pending setup keeps the canonical
address. Private apps still sign in through central Auth; public apps retain
optional signed-in features through the SDK. On a custom hostname, the
same-origin sign-in bridge automatically continues in the same tab through
`auth.<base-domain>` and returns through a one-time, claim-bound callback. The
resulting host-only app-session cookie avoids another Auth round trip on later
app loads; the central Auth cookie is never shared with or readable by the
custom hostname. Email-link approval happens in the email window and session
completion resumes in the initiating Auth tab. Website domains do not change
email addresses or transfer browser storage and Web Push permissions. Removal
disconnects immediately; a different hostname can be added during cleanup,
while reuse of the retired hostname must wait. See
<https://docs.opencloud.ai/guides/custom-domains>.

Do not invent `opencloud.auth.signOut()`. A visible custom-domain Sign out
action sends a same-origin POST to `/_opencloud/sign-out`, then calls
`location.reload()` on success to clear in-memory SDK state and connections.
It revokes the current browser's parent session and aliases, retaining other
devices. Handle a failed response before reloading.

## Use the stable JavaScript SDK

Every frontend imports the deployment-pinned singleton from one stable path:

```js
import {
  opencloud,
  OpenCloudError,
  OPEN_CLOUD_SDK_VERSION,
} from "/_opencloud/sdk.js";
```

Do not fetch `/_opencloud/config`, import an immutable version path, construct a
client, or read runtime credentials in application code. The edge maps the
stable module and `/_opencloud/sdk.d.ts` declarations to the exact artifact
pinned by `runtime.sdk.version`. Inspect the active pin operationally
without printing runtime credentials:

```bash
opencloud app sdk-inspect "$APP_ID"
```

Use only these public methods:

| Interface | Methods                                                                                        |
| --------- | ---------------------------------------------------------------------------------------------- |
| Version   | `OPEN_CLOUD_SDK_VERSION`                                                                       |
| App       | `app.info()`                                                                                   |
| Auth      | `auth.currentUser()`, `auth.requireUser()`, `auth.signInUrl()`                                 |
| Data      | `data.table(name).list/getById/create/createMany/updateById/deleteById`                        |
| Files     | `files.upload/info/download/save/replace/remove/attach`                                        |
| Functions | `functions.call(name, input?)`, `functions.stream(name, input?)` for `user`/`public` Functions |
| Notifications | `notifications.status()`, `notifications.subscribe()`, `notifications.unsubscribe()`         |
| Realtime  | `realtime.subscribe(topic, handler)`, `realtime.publish(topic, event, payload)`                |
| Telemetry | `telemetry.summary/increment/gauge`                                                            |
| Client    | `dispose()`                                                                                    |

The SDK returns parsed values, managed file metadata, or typed streams—not raw
HTTP responses. It owns config discovery, app identity, Function access mode,
bearer tokens, cookie forwarding, refresh, file routing, and Realtime protocol
state. Never decode JWTs, persist token material, call raw REST/Storage/Function
endpoints, or construct buckets and object paths.

Handle expected failures through the typed error contract:

```js
try {
  await opencloud.data.table("items").create({ title });
} catch (error) {
  if (error instanceof OpenCloudError) {
    renderProblem(error.code, error.requestId, error.retryable);
  } else {
    throw error;
  }
}
```

An `OpenCloudError` has `code`, `surface`, `status`, `requestId`, `retryable`,
and optional `details`. Show a safe product message and retain the request ID
for diagnostics. Do not parse response text or expose raw platform details.

The browser SDK deliberately exposes high-level data and managed Files
operations instead of request paths. Use `opencloud.data.table(...)` and
`opencloud.files` with opaque file IDs. Never construct REST paths, Storage
buckets, object names, owner prefixes, URLs, or authorization headers.

## Build data with RLS

Write unqualified DDL; OpenCloud selects the app schema.

For owner-isolated records:

```sql
create table items (
  id uuid primary key default gen_random_uuid(),
  owner_id uuid not null default auth.uid(),
  title text not null check (length(title) between 1 and 200),
  created_at timestamptz not null default now()
);

create policy items_owner_access
  on items for all
  using (owner_id = auth.uid())
  with check (owner_id = auth.uid());
```

For records shared by admitted app members, add a permissive business policy:

```sql
create policy items_member_access
  on items for all
  using (true)
  with check (true);
```

OpenCloud forces RLS and combines business policies with a restrictive app
boundary. Deployments execute the complete history in a disposable constrained
schema on the pinned PostgreSQL runtime before touching live app data.

Read <https://docs.opencloud.ai/reference/sql> before using nontrivial SQL.

Use the bounded data API. It validates identifiers, owns authentication and
parses the result:

```js
const items = opencloud.data.table("items");
const rows = await items.list({
  select: ["id", "title", "created_at"],
  orderBy: { column: "created_at", direction: "desc" },
  limit: 50,
});
const created = await items.create({ title: "Review evidence" });
await items.updateById(created.id, { title: "Reviewed evidence" });
```

`list` returns at most 1,000 rows (`limit` 1 to 1000); page through more with
`offset`. The exact results are `list -> Row[]`, `getById -> Row | null`, `create ->
Row`, `createMany -> Row[]`, `updateById -> Row | null`, and `deleteById ->
boolean`. There is no `.rows` wrapper and no `insert`, `upsert`, or broad
mutation alias.

Reads use the current user when present and can follow deliberately public RLS.
Writes require a user. Mutate only through `updateById` and `deleteById`; never
build filters that can accidentally update or delete multiple rows.

For platform-managed App Admin permissions, use the existing Data SDK:
`const [access] = await opencloud.data.table('opencloud_app_access').list();`
`access.is_app_admin` reflects the current app Edit permission, including access
to its normal Agent conversation. Functions use the same read through `data`.
This platform-managed, read-only view needs no migration or SDK upgrade.
Enforce protected reads and writes with `auth.is_app_admin()` in SQL policies;
the browser value only controls presentation. No second role table or
first-visitor bootstrap is needed. Installation-wide review access does not
grant this permission. Existing RLS and Files policies still apply.

Development identities are temporary and change in each fresh sandbox. Never
copy a user ID from development inspection into application source or RLS.
Use supported authenticated context for user-owned records. System ingestion
has no user identity; model its app-owned records and explicit read policy
without assuming that the system caller is the owner.
In a development sandbox, synthetic user A is App Admin; B and C are ordinary
users. For app-owned system-ingested records, give `auth.is_system()` one policy
for all commands (`using (auth.is_system()) with check (auth.is_system())`) and
let `auth.is_app_admin()` read/manage them instead of assigning a fake owner.
`create` and `update` return the written row, so a system write also needs read
access; an insert-only system policy fails with a row-level security error.

Rows added in a development session stay there. When the app can't be used
until it is set up, such as a rota that needs its members or a shop that needs
its menu, put the details the person gave, in the chat or in the preview, into
a migration so the live app starts set up, and ask for any that are missing
before you deploy. Show a setup form only to the app's admins
(`access.is_app_admin`); everyone else sees who will set the app up. Let admins
remove any entry the app stores, such as a wrong booking, from the app's own
screen.

## Apps that manage their own sign-in

Prefer OpenCloud accounts. Only when the user asks for an app that signs in
its own users, such as a mobile app's API, pin SDK 2.7.0, keep the app
public, and use `access: public` Functions. Read the app's own credential
with `headers?.get("authorization")` (present only when it is not one of
OpenCloud's credentials) or a custom header, verify it in the Function with a
key from `secrets` or the identity provider's published keys, then call
`data.asAppUser(userId)`. Write owner policies with `auth.app_user_id()`,
which is text, never with `auth.uid()` or an anonymous policy:

```sql
create policy notes_owner on notes for all
  using (owner_id = auth.app_user_id())
  with check (owner_id = auth.app_user_id());
```

The app's own `Authorization` reaches the Function from browsers signed in to
OpenCloud too: the development preview, verification browsers, and an owner
testing their own app. Such a call never uses the browser session, so `user`
stays `null`. A Function that also needs the OpenCloud user must read the
app's credential from a custom header instead; an `access: user` Function
rejects the call with `OPENCLOUD_USER_REQUIRED`. This applies to public apps
only. On a private app and its development hosts, a signed-in browser's
session replaces `Authorization`. Private apps also reject `asAppUser` with
`APP_USER_IDENTITY_REQUIRES_PUBLIC_APP`. App-managed users get no per-user
Files, Realtime, Web Push, or calling-user integrations. Never issue tokens
that claim one of OpenCloud's issuers. See
<https://docs.opencloud.ai/guides/auth#apps-that-manage-their-own-sign-in>.

## Query declared PostgreSQL searches

The unpublished SDK 2.4/platform candidate supports `data.search` inside
Functions. Pin `runtime.sdk.version: 2.4.0` and declare `data.search` entries
with a logical `name`, app `table`, selected columns including `id`, optional
equality `filterColumns`, and `fullText` and/or `vector` configuration. App
migrations create a `tsvector` column and/or `vector(n)` column and indexes;
the platform owns pgvector installation. See
<https://docs.opencloud.ai/reference/manifest#postgresql-search-declarations>.

```ts
const { hits } = await data.search("document-chunks", {
  mode: "hybrid", query: input.question, vector: queryEmbedding,
  model: "potion-128-v1", where: { document_id: input.documentId }, limit: 10,
});
```

Use `fullText` with query only, `vector` with vector/model, or `hybrid` with
both. Enforce the declared model/dimensions; re-embed stored content when the
model changes. Query text is bounded to 2,048 characters, results to 50 hits,
vectors to 2,000 finite float32-compatible dimensions with nonzero norm, and
filters to declared scalar equality columns. Search keeps caller Authorization
and RLS; private-file delegation adds no database authority. The browser SDK
has no search method; invoke a declared Function.

Show returned passages and separate full-text score/rank, cosine
distance/similarity/rank and hybrid `fusionScore`. Hybrid uses reciprocal-rank
fusion with k=60, not addition of unlike component scores. Vector retrieval is
exact; HNSW DDL is supported but approximate acceleration is not used. Never
present similarity as confidence or word highlighting as an embedding
explanation. Keep staged/obsolete chunks invisible through document-revision
RLS and invalidate app-derived results when the document workflow replaces or
deletes a source. Direct Files changes do not do that automatically.
Read <https://docs.opencloud.ai/guides/functions-cron#postgresql-search> for
limits, errors and indexing lifecycle before implementing this capability.

## Independent app Agent tasks

For SDK 2.5.0 `agentTasks`, read the scheduled agent task guide before authoring
the manifest. Do not set a task token limit. New app agents, independent tasks,
and their children have no per-run token cap; OpenCloud-funded usage shares the
owner's weekly account AI allowance across apps and Functions. Keep tasks
focused, choose appropriate timeouts and batches, and verify durable results.
Account allowance exhaustion requires renewed funding or the weekly reset;
changing prompts or spawning another agent does not create more allowance.

For new interactive screenshot tasks on the v3 Agent runtime, omit `capture`
and tell the task agent to use the installed Playwright CLI and
`opencloud-browser` skill to open the supplied URL, dismiss cookie banners,
wait for the content, take a PNG, upload it, and submit a result. URL field
names follow the app schema. The legacy `capture: website_screenshot` option
still automatically captures before the Agent runs and requires `input.url`;
do not use it for new tasks that need interaction.

For either screenshot workflow, the result Function's strict input schema
must declare **both `submissionKey` and `screenshotFileId`**, alongside your
app fields. Use `schema.uuid()` for `screenshotFileId`; the worker uploads the
captured PNG and passes the returned managed File ID. Validate it with
`files.info({ id: input.screenshotFileId })`, persist the reference with the
observation, and return the same `screenshotFileId` in every successful receipt,
including unchanged and idempotent-replay receipts. For new tasks return exactly
`{schemaVersion:2,submissionKey,resultId,committed:true,screenshotFileId}` after
saving, where `resultId` identifies the saved row. Use the outer assignment's
platform-issued submission key, not an app key nested inside its input. Legacy
version-1 campaign receipts remain supported. Result Functions that inspect
user-scoped Files must use `access: "user"` and normal caller/RLS checks:
the owner's task CLI supplies authenticated user authority, whereas a system
Function has no user identity. Never remove the field to
work around input validation. Follow the complete receipt contract in the
scheduled task guide. Before release, test the actual result Function with an
uploaded PNG and the full worker payload, then assert the saved row and receipt;
a hand-written test that omits the screenshot field does not test this contract.

Independent app tasks share the app's workspace queue with the builder. A task
submitted by the active builder cannot execute until that builder ends its
turn. After verifying the result Function in development and admitting a live
task, finish the builder turn promptly and report verification as pending.
Do not poll or sleep waiting for that task inside the builder run, and do not
claim that admission proves successful saving. Check the task's reconciled
receipt and retained data after the builder has released the workspace.

## Use Files, Realtime, Functions, background jobs, and telemetry

Upload a browser `Blob` or `File` directly. OpenCloud returns an opaque ID and
owns paths, authorization, size enforcement, names, content types, and
idempotency:

```js
const uploaded = await opencloud.files.upload({
  data: file,
  name: file.name,
  contentType: file.type || "application/octet-stream",
  onProgress: ({ percent }) => renderUploadProgress(percent),
});
```

`upload` and `replace` automatically retry one transient failure with the same
private idempotency key. Do not invent or manage a retry key in app code.

When a file belongs to a database record, prefer the compound helper. Create
the table with `file_id`, `file_name`, `file_type`, and `file_size` columns (or
provide an explicit `columns` mapping):

```js
const { file: storedFile, record } = await opencloud.files.attach({
  data: file,
  name: file.name,
  table: "item_attachments",
  values: { item_id: itemId },
});
```

`attach` reconciles an ambiguous metadata write and cleans up a definite
failure. If it throws `FILE_ATTACHMENT_INCOMPLETE`, retain the returned file
details for an explicit cleanup retry. Store the opaque ID; never store or
construct a bucket/object path, send Storage headers, or request S3
credentials. Use `download`, `save`, `replace`, and `remove` with the returned
file object or ID.

Subscribe to private Realtime topics with a logical purpose:

```js
const unsubscribe = await opencloud.realtime.subscribe(
  "items",
  ({ event, payload }) => {
    if (event === "changed") void reloadItems(payload);
  },
);
void opencloud.realtime
  .publish("items", "changed", {
    reason: "item-created",
  })
  .catch((error) => console.warn("Realtime notification skipped", error));
```

Call `unsubscribe()` when the view unmounts and `opencloud.dispose()` when the
app tears down. Realtime is best-effort invalidation, not durable truth. Check
`app.info().capabilities.realtime` before subscribing, never await publication
from a primary CRUD action or verifier cleanup, persist through `opencloud.data`
first, and let recipients reload durable truth. Send identifiers, not secrets
or full records. Query current controls again after rendering replaces nodes.

Write Deno-compatible Functions with the first-party server boundary. There is
no global `Buffer` or `require`: use web APIs such as `TextEncoder` and
`crypto.subtle`, or import Node modules explicitly (`node:crypto`). Each
Function is deployed on its own with only its entrypoint's directory: keep
helpers inside it, import exact file names with extensions, and copy a shared
helper into each Function that uses it.

```ts
import { defineFunction, errors, schema } from "@opencloud/server";

export default defineFunction({
  input: schema.object({ itemId: schema.uuid() }),
  handler: async ({
    input,
    user,
    data,
    secrets,
    log,
    requestId,
    environment,
  }) => {
    if (!user) {
      throw errors.unauthorized("SIGN_IN_REQUIRED", "Please sign in");
    }
    const item = await data.table("items").getById(input.itemId, {
      select: ["id", "title"],
    });
    if (!item) throw errors.notFound("ITEM_NOT_FOUND", "Item not found");
    log.info("item loaded", { itemId: input.itemId });
    return {
      item,
      requestId,
      environment,
      secretPresent: Boolean(secrets.get("AI_API_KEY")),
    };
  },
});
```

`defineFunction` accepts exactly `{ input, handler }`; `input` is already
parsed. Handler-only definitions and `input.json/parse/text` do not exist.
Use `ai.generateText` for completed text, `ai.streamText` for incremental UTF-8
text bytes, `ai.generateObject` with a `schema` for a validated object, or
`ai.generateImage({ prompt })` for PNG bytes. Return an `ai.streamText` result
in a no-store plain-text `Response` with `X-Accel-Buffering: no`, and consume it
with browser `functions.stream`. Text, stream, and object calls accept managed
PNG, JPEG, WEBP, non-animated GIF, or PDF Files as
`attachments: [{ type: "file", file: fileRef, detail? }]`; prefer this opaque
reference over downloading and copying bytes through the Function. PDFs are
limited to one managed File, 20 pages, and 20 MiB. Use
`{ type: "image", data: Uint8Array, contentType, detail? }` only for generated
or otherwise in-memory bytes. Remote URLs are not accepted. Image inputs are
limited to four and 15 MiB each; all attachments share a 20 MiB total.
Model selection, provider envelopes, idempotency, retries, and timeouts remain
private platform behavior. `await ai.generateText(...)` returns a plain string:
use `text.trim()`, never `result.text`, `result.output`, or a provider envelope.

AI calls run only in the live app: development previews and verification
refuse them with `AI_UNAVAILABLE_IN_DEVELOPMENT`. Keep the page working without
the AI result, for example "The summary appears once the app is live", and
don't make tests depend on it. Tell the person in those words, and once the app
is live, check the AI feature with one real item and say what it produced.

When a Function reports its pinned SDK version, import
`OPEN_CLOUD_SDK_VERSION` from `@opencloud/server`; never hard-code the version
string.

The Function context contains `input`, `user`, `job`, `data`, `files`,
`ai`, `email`, `notifications`, `jobs`, `integrations`, `secrets`, `log`, `requestId`, and
`environment`. SDK 2.3.0 and later also provide `http`, the read-only route context or
`null` for an ordinary invocation. SDK 2.6.0 and later also provide
`integration`: in an integration provider's operation, sync, webhook, or OAuth
Function it names the connection (`connectionId`, `trigger`, `operation`,
`consumerAppId`) and offers `emit`; otherwise it is `null`. SDK 2.7.0 and
later also provide `headers`: read-only client request headers (`get`, `has`,
`entries`, lowercase names) for browser, app, and routed calls, or `null` for
platform invocations. Cookies, OpenCloud credentials and internal headers,
forwarding and client-address claims are removed; header values are client
input, never identity.
These are high-level,
invocation-scoped capabilities; raw database clients, buckets, provider keys,
and OpenCloud bearer tokens are not exposed. The outer platform gateway allocates a
request ID before module loading and catches imports, rejected promises,
timeouts, invalid responses, and platform-call failures. Unknown production
errors are generic; dev diagnostics are bounded and redacted.

Declare browser access as `user` or `public` in the manifest. Declare every
cron, queue, inbound-email, or platform-only Function as `system`; the browser
SDK rejects it with `FUNCTION_SYSTEM_ONLY`. Call a browser Function with
`opencloud.functions.call(name, input)` for parsed output or
`opencloud.functions.stream(name, input)` for a byte stream; the SDK selects
the correct auth mode from the manifest.

Cron Functions receive `{ source: "opencloud.cron", name, scheduledAt }` as
their JSON input. Declare those three fields in the strict input schema;
`schema.object({})` rejects a real invocation. Use the scheduled occurrence
in idempotency keys, and preserve the user's named timezone (for example,
09:00 UK time is `0 9 * * *` with `timezone: Europe/London`). Test a full cron
payload and inspect a real invocation before claiming automatic work succeeds.
Development runs no schedules: with the CLI, run the Function once with
`opencloud app dev invoke` and that payload. First add the rows it reads, such
as a renewal due next week, with
`opencloud app dev data "$APP_DIR" <table> create --values '<json>'` or
`create_dev_data_row`, so the run has something to send. MCP has no tool that runs a
Function directly, so put the scheduled work, such as a reminder email, in a
module that the cron Function and an admin-only `user` Function both call, and
give admins a "Send a test now" button. Press it in the preview, then read the
kept email with `list_dev_email_captures`. Once the app is live, the owner
can run a schedule once with Run now on the app's Scheduled jobs page in the
dashboard. Only an owner Agent session can run it, with
`opencloud cron invoke <app-id> <cron-name>`. Read past runs with
`opencloud cron history <app-id>`.

Cron occurrences survive app deployments and worker restarts. Recovery runs
the latest eligible missed occurrence within 24 hours, coalescing earlier
ticks. Waiting for the app does not spend an attempt; proven failures allow
up to four execution attempts, and unknown outcomes are not blindly retried.
Changed or disabled schedules invalidate their obsolete unstarted work.

For change monitoring, retain structured source facts and compare a canonical
representation. Generated prose can paraphrase unchanged facts; never use its
exact string as the change detector. Verify an unchanged capture produces no
change event or email, and a changed source produces one idempotent event.

Declare Postgres-backed background queues under `queues`; each targets a
declared `system` Function. Producers call
`jobs.enqueue(queue, input, { idempotencyKey, delaySeconds? })`, and every
enqueue requires a stable idempotency key: up to 128 letters, digits, dots,
underscores, colons or hyphens, such as `signup:<id>`. `email.send` needs at
least 8 characters. Consumers receive the JSON
object as `input` and `{ id, queue, attempt }` as `job`. Delivery is
at-least-once: make side effects idempotent and throw `errors.unavailable` only
for retryable failures. `jobs.get(id)` reports `queued`, `running`,
`retry_wait`, `succeeded`, or `dead_lettered`. Queue payloads are
limited to 64 KiB, delays to seven days, and active work to 10,000 jobs per
app/environment/Function namespace. Terminal job status is retained for 14
days. Queue concurrency, attempts, delay, backoff, and timeout come from the
manifest and are pinned at enqueue. A suspended, archived, or transiently
non-active production app defers work without consuming an attempt. Removing a
queue declaration dead-letters its pending work with `QUEUE_NOT_ACTIVE`;
restoring the queue does not redrive it.
There is no strict FIFO, priority, manual acknowledgement, cancellation, or
dead-letter redrive API in the first release.

For private file processing, pin the paired SDK to `2.4.0` and enqueue from an
authenticated producer Function with explicit file inputs:

```ts
await jobs.enqueue("index-document", { fileId }, {
  idempotencyKey: `index:${fileId}:${revisionId}`,
  files: { read: [fileId] },
});
// In the declared system consumer:
const pdf = await files.download(fileId);
```

Keep `files.access: user`; delegation does not require sharing the library.
Pass one to sixteen UUIDs, at most 128 MiB aggregate; existing per-file limits
still apply. Selection is normalized for idempotency. The platform pins the
actual content generation: replacement, even with identical bytes, produces
`FILE_INPUT_CHANGED`; enqueue a new revision with a new key. Grants last at
most 14 days, with separate bounded access for each attempt. Logout alone does
not cancel them, but current file, principal, app and retained domain/token
access must remain valid. Delegation permits only downloading selected content,
not metadata, listing, mutation, further delegation or user impersonation.
Omit `files` for ordinary jobs and preserve their existing SDK pin.

System invocations keep `user` and `auth.uid()` null; an app-table RLS policy
must explicitly use `auth.is_system()` for rows that a queue, cron, inbound
email, Slack, Telegram, Asana, or integration event consumer may access, including the rows it creates. Never use `auth.uid() is null` for this because it
also grants ordinary anonymous access in a public app. The platform separately
binds the signed system identity to the current app or development namespace.

Declare app-owned identities under `email.addresses`. A receive-capable alias
references a Function with `access: system`; that same allocated address is
used for sending and receiving. Function code sends only from a declared alias
with `email.send(message, { idempotencyKey })`. Provider credentials and SMTP
are never exposed. For example, a queue consumer that confirms a booking:

```ts
import { defineFunction, schema } from "@opencloud/server";

export default defineFunction({
  input: schema.object({ bookingId: schema.string(), email: schema.string() }),
  handler: async ({ input, email }) => {
    await email.send(
      {
        from: "bookings", // a name declared under email.addresses
        to: input.email,
        subject: "You're booked: Saturday taster class",
        text: "See you on Saturday at 10:00 at the studio.",
      },
      { idempotencyKey: `booking-confirmation:${input.bookingId}` },
    );
  },
});
```
 Development Functions capture every outbound message and
support synthetic `.test` inbound injection through the email CLI commands or
MCP operations.

Development Functions remain dormant until an `app dev invoke` CLI command,
deliberate preview interaction, or declared development event calls them. Dev
Functions get the isolated dev schema, isolated values for secrets declared as
`generated`, no owner-provided production values, and no cron triggers. A
deliberate enqueue automatically wakes its consumer in the same isolated
namespace. Before verification, exercise every Function through its intended
path: direct invocation or browser action for ordinary Functions, enqueue for
queue consumers, and synthetic injection for inbound email. Inspect
`list_dev_invocations` or `app dev requests`;
the latest invocation of each must succeed. Repeat these checks after any sync.

Captured delivery is a real development SDK result. Do not catch an email,
notification, or integration error only in development and return success, or
add an app-side injection bypass to satisfy verification. Persist and display
the actual pending or failed state, and verify the intended path again in a
fresh session. Only a successful SDK result justifies a completed-effect claim.

Declare native Web Push with `notifications.webPush: true` and SDK `2.1.0` or later.
Optionally set a same-origin `notifications.icon` as the app-wide default; if
it is omitted, OpenCloud supplies its logo.
Browser code inspects `notifications.status()` without prompting and calls
`notifications.subscribe()` only from a visible user gesture; never prompt on
page load. A Function sends to an authenticated app user with
`notifications.send({ userId, title, body?, path?, icon? }, { idempotencyKey })`.
Keep `path` and `icon` same-origin. A per-message icon overrides the manifest
default. Development captures visible notification payloads for
`list_dev_notification_captures` and never contacts a browser push provider.
For iOS acceptance, use Safari 16.4 or later and launch the web app after adding
it to the Home Screen. iOS and iPadOS currently ignore the notification payload
icon and display the installed app icon; provide that icon in the web app
manifest or an `apple-touch-icon`, which takes precedence when both exist.

For every brokered integration, creating or authorizing an account connection
and completing provider consent remain human account actions. With
`account: app`, an exact-app owner Agent may list the app's integration
declarations and current bindings plus bounded eligible-connection metadata
and opaque IDs for the exact app's declared slots, then bind, configure, or
unbind an already authorized connection through the owner-parity `integration`
commands after that CLI release is pinned. This app-context projection includes
only the delegated owner's connections usable by declared `account: app`
slots. It cannot enumerate the general account catalog, manage account
connections, expose raw credentials, reveal undeclared providers, include
`calling_user` connections, or cross into another owner's connections. A
`calling_user` binding always remains that calling user's action and authority.
Ordinary human-directed flows may continue to use App Integrations in the
browser.

Built-in providers work only where the installation has configured them. If
connecting reports that a provider is not configured on this installation, tell
the owner; do not switch providers or call the external system directly.

For Google Workspace, declare a named `google-calendar`, `google-drive`,
`google-sheets`, `google-docs`, or `google-slides` integration slot with
`account: app | calling_user`, `cardinality: one | many`, and only its
provider-scoped read/write capabilities. Functions use the bounded typed
`integrations.calendar/drive/sheets/docs/slides(slot)` clients; raw Google
requests and `batchUpdate` bodies are not public APIs. A human connects one
reusable Google account in account Integrations and authorizes the available
resources; Calendar binds an exact calendar under the general binding rule
above. Never ask for, receive, store, log, or return an OAuth token, Google
subject, or raw provider response. Obtain an opaque connection ID only from
the exact-app projection above, never from conversation or the general account
catalog. Development uses deterministic synthetic Workspace data; production
needs an active release and active binding.

For read-only Google reporting on SDK 2.2.0 or later, declare
`google-analytics` with
`analytics.reports.read`, `google-search-console` with
`search.performance.read`, or `google-ads` with `ads.reporting.read`. These
slots use `account: app` and bind one exact GA4 property, verified Search
Console site, or directly accessible non-manager Ads customer. Functions use
the provider-independent `integrations.use(slot)` client and call the bounded
`analytics.reports.run`, `searchConsole.performance.query`, or
`googleAds.campaigns.report` operation. The broker validates the provider,
capability, binding, and input, so adding a future integration operation does
not require a core SDK release. Never accept a raw property, site, customer,
GAQL query, provider request, or token. Report ranges are bounded to 366 days,
and development returns deterministic synthetic data without contacting
Google unless the slot is switched to live.

The typed methods are Calendar `bindings/listEvents/createEvent`; Drive
`bindings/listFiles/getFile/createFile/createFolder`; Sheets
`bindings/getSpreadsheet/createSpreadsheet/getValues/updateValues/appendValues`;
Docs `bindings/getDocument/createDocument/appendText`; and Slides
`bindings/getPresentation/createPresentation/addSlide`. Pass an opaque
`bindingId` only to select among `cardinality: many` bindings.

For Asana, declare `provider: asana` and only the needed `asana.*`
capabilities for tasks, assignees, sections, custom fields, attachments,
stories/comments, and events. Every binding selects one exact project.
Functions use `integrations.asana(slot)` with bounded
`bindings/listTasks/getTask/createTask/updateTask`,
`listAssignees/assignTask`, `listSections/moveTask`,
`listCustomFields/updateCustomFields`,
`listAttachments/createExternalAttachment/uploadAttachment/downloadAttachment`,
and `listStories/createComment` methods. A section is a project grouping: a
list heading or board column. Never accept arbitrary Asana API calls, tokens,
workspace/project IDs from conversation, or tasks outside the bound project.
Task mutations that return current task state also require `asana.tasks.read`.
Task results expose only the bound-project membership and independently gate
assignee, section, and project custom-field details behind their read
capabilities. Development data is deterministic and synthetic.

Incoming Asana changes require `asana.events.receive`,
`asana.tasks.read`, `account: app`, and `events.function` naming a declared
`system` Function. The handler receives an `OpenCloudAsanaEvent` with
normalized events, up to 20 current contained task snapshots, and a
`tasksTruncated` marker. Use its `id`
idempotently; app writes can be observed again as incoming changes. Account
connection and exact-project authorization are human actions in OpenCloud; the
app binding follows the general rule above.
Installation setup requires an Asana OAuth app with Full Permissions because
not every endpoint has a granular scope; OpenCloud still enforces every
manifest capability and project boundary.

For HubSpot CRM, declare `provider: hubspot-crm`, `account: app`, and only the
needed `crm.contacts.read|write`, `crm.companies.read|write`,
`crm.deals.read|write`, `crm.owners.read`, `crm.pipelines.read`,
`crm.notes.write`, and `crm.associations.write` capabilities. Functions use
`integrations.crm(slot)`: contacts, companies, and deals expose
`get/search/create/update/upsert`; owners expose `list`; pipelines expose
`listDeals`; notes expose `create`; associations expose `create`. Association
writes also require the write capability for both source and target record
types. A human authorizes a reusable portal in account Integrations; its app
binding follows the general rule above. Never ask for a HubSpot token, accept
raw requests or a connection ID outside the exact-app projection, add delete
operations, invent webhook events, or use `account: calling_user`. Development
uses deterministic synthetic CRM data and does not contact HubSpot unless the
slot is switched to live.

For open banking, declare `provider: gocardless-bank-account-data` with only
`bank.accounts.read`, `bank.balances.read`, and/or `bank.transactions.read`.
Functions use `integrations.bankAccountData(slot)` with bounded
`bindings/account/balances/listTransactions` methods. A human selects their
institution, completes bank-hosted consent, and authorizes exact accounts; the
app binding follows the general rule above. Never ask for bank login details,
GoCardless credentials, requisition IDs, account IDs,
or raw provider access. Development data is synthetic and no bank is contacted.

For Wise transactions, declare the technical provider
`provider: wise-balance-webhook`, `account: app`, and only
`payments.received.reconcile`. Functions use
`integrations.receivedPayments(slot).bindings/reconcile` for expected incoming
credits. The owner creates exactly one **Account deposit events** webhook in
Wise using the notification URL shown under account Integrations and tests it;
the app binding follows the general rule above. Do not ask them to create
Transfer update or Transfer issue events. Never request a Wise login, API token, OAuth grant,
certificate, balance, raw transfer ID, or raw transaction feed. Reconcile only
an exact expected amount/currency/time window; a result of `ambiguous` always
requires human review and must never trigger fulfillment.

For Slack, declare `provider: slack`, `account: app`, and only
`slack.messages.send` and/or `slack.messages.receive`. Receiving also declares
`events.message.function` targeting a declared `system` Function. Functions
use `integrations.slack(slot).bindings/sendMessage`; inbound handlers receive
`OpenCloudSlackMessageEvent`. A human connects the reusable workspace and
invites the managed `@OpenCloud` identity to an exact channel. The app binding
selects that channel and `mention` or `all_messages` under the general rule
above. Every outbound message is visibly labelled with the OpenCloud app name.
Inbound delivery is at least
once; use the stable event `id` to make side effects idempotent. Never ask for
a Slack token, handle raw provider events, choose arbitrary channels in code,
implement slash commands, or treat a Slack mention as authorization for a
sensitive action.

For Telegram, declare `provider: telegram`, `account: app`, and only
`telegram.messages.send` and/or `telegram.messages.receive`. Receiving also
declares `events.message.function` targeting a declared `system` Function.
Functions use `integrations.telegram(slot).bindings/sendMessage`; inbound
handlers receive `OpenCloudTelegramMessageEvent`. A human uses the app's
one-time link to add an exact private chat or group. The app binding selects an
owned paired chat and `directed` or `all_messages` under the general rule above.
`cardinality: one` selects one chat at a time; `many` can bind several. Private
chats need no bot administrator access. Directed groups keep the bot as a
member; all-message groups grant it minimal administrator access in that exact
group while Telegram privacy mode stays enabled. Every outbound message is
labelled with the OpenCloud app name.
Inbound delivery is at least once; use the stable event `id` to make side
effects idempotent. Never request a bot token or raw chat ID, handle raw
Telegram updates, choose a chat in code, disable privacy mode, or treat a
Telegram sender ID, mention, command, or reply as authorization for a
sensitive action.

Development integration slots return deterministic synthetic data by default.
To confirm real behaviour before deploying, switch one app-account slot in one
development session to live with MCP `set_dev_integration_mode`; it uses the
app's production binding, reaches the real system, and is audited. Prefer
reads, avoid live writes unless the user asked for that behaviour, and switch
the slot back to `fake` afterwards. `calling_user` slots and verification
sandboxes always stay synthetic, and development email is always captured.

Apps can use integrations that another app of the same organisation or account
publishes. Call MCP `list_custom_integrations` for the published contracts, then declare
a slot with `provider: custom`, `integration: <name>`, `account: app`, and only
the needed lowercase dotted capabilities on SDK 2.2.0 or later. Functions call
`integrations.use(slot).call(operation, input)`; typed provider errors keep
their codes. Entering credential values or signing in is a human action in
OpenCloud; never ask for credentials or tokens. Binding an existing connection
follows the general app binding rule above. Use `account: calling_user` when
each person must use their own account: a call without that person's connection
fails with `INTEGRATION_CONNECTION_REQUIRED` and a `details.connectUrl` to show
them, and only the person can bind it. To react to provider changes, give an
`account: app` slot `events: { function: <system Function>, types?: [...] }`;
delivery is at least once, so handle the event `id` idempotently, and test the
handler with MCP `inject_dev_integration_event`.

To publish an integration, add `provides.integration` to a schema-3 manifest:
a kebab-case `name` unique within the organisation or account, `title`, `description`, credential
fields (secret and required unless `secret: false` or `optional: true`),
capabilities, and operations. Each operation has a dotted camelCase name, a
description, a declared capability, a declared `system` Function, optional
JSON Schema `input`/`output` documents, and a static `fake` output that
consumers receive in development. The operation Function receives the
consumer's input and reads credential values with `secrets.get(NAME)`; never
log, store, or return them. For OAuth systems, declare
`authorization: { type: oauth2, authorizationUrl, clientId, scopes, exchange,
refresh }`: the exchange and refresh `system` Functions redeem codes and refresh
tokens with the app's own client secret, and operation Functions read the
access token with `secrets.get("OAUTH_ACCESS_TOKEN")`. On SDK 2.6.0, optional
`sync` (a scheduled `system` Function run per bound connection), `webhook` (a
`system` Function receiving each connection's webhook URL requests), and
declared `events` let the provider keep per-connection data and publish
changes with `integration.emit({ type, id, data })`. Partition that data by
`integration.connectionId` and `auth.connection_id()` in RLS policies. Test it
in the provider's development session with a test connection selected through
`set_dev_integration_test_connection`, preferring a sandbox or read-only
connection. The hosted Agent's `organisation-integrations` skill and
<https://docs.opencloud.ai/getting-started/agent-integrations> give the full
workflow, commands, and Function contracts.

Declare each Function secret in the manifest with one mode:

- `generated`: OpenCloud creates and retains a strong production value
  automatically and supplies an isolated synthetic value in development;
- `required`: the owner must enter a value before production; or
- `optional`: the owner may enter a value, but its absence never blocks a
  release and `secrets.get(name)` returns `undefined`.

Do not call a setup command for `generated` secrets. In an ordinary
human-directed or legacy Agent session, rotate one only when the user
authorized that change. An exact-app owner Agent may rotate an assigned-app
secret when its work requires it without seeking another operation-specific
approval. With MCP use `generate_secret`; with the CLI use:

```bash
opencloud secret rotate "$APP_ID" INTERNAL_SIGNING_KEY
```

For a `required` or `optional` owner-supplied value, call MCP
`create_secret_entry_link` or use:

```bash
opencloud secret configure "$APP_ID" AI_API_KEY
```

Give the returned URL to the user. Do not ask them to paste the value into the
agent conversation. Return only a presence flag, version marker, or one-way
digest for a secret.

Once the owner-parity CLI release is pinned, an exact-app owner Agent that
already has a value in a protected source may replace it with:

```bash
opencloud secret set "$APP_ID" AI_API_KEY \
  --idempotency-key "$IDEMPOTENCY_KEY" < "$PROTECTED_SECRET_FILE"
```

`secret set` reads the value only from standard input. Never place it in an
argument, JSON file, command history, tool output, or report. Reuse the same
idempotency key after an ambiguous response, then confirm only secret metadata.
If the human owner is the only source of the value, use `secret configure`
instead of asking them to reveal it to the Agent.

An AI Function runs on the AI account the app's owner chooses on the app's AI
page (`/apps/<appId>/ai` in the dashboard); never ask for, substitute, or copy
a provider key or the coding agent's credential. Some installations accept
only OpenAI or Anthropic API keys for app AI. If an invocation reports
`app_ai_credential_not_selected`, `app_ai_own_use_required`,
`app_ai_api_key_required`, or `ai_capacity_unavailable`, give the owner its
message and that page once, and resume only after they act. User-provided
AI instead requires every authenticated caller to connect a default AI
connection and explicitly allow the exact app in OpenCloud's trusted AI
permission window before its first AI request. Open
`/_opencloud/ai-consent?return_to=<encoded same-origin path>` only as a
top-level popup navigation initiated by the user's AI action; never fetch,
submit, frame, imitate, or auto-approve that Auth-origin form. Wait for the
`opencloud:ai-consent` result from that exact popup before invoking the
Function. The inference boundary rejects user-token use without the stored
app-specific grant. Cron, inbound-email, anonymous, and other system calls
cannot use AI in that mode. Keys and provider tokens remain inside the encrypted
integration vault and short-lived inference runtime; never declare them as app
secrets or ask for them in conversation. The owner also selects the app's text
model and default reasoning effort on the app's AI page.

Use exact telemetry fields:

```js
const summary = await opencloud.telemetry.summary();
const rest = summary.activity.surfaces.rest;
const freshness = summary.activity.telemetry;
```

All six surfaces are always present. `usage` can be null. Treat unavailable,
missing, or truncated activity honestly; never label absence as healthy. Read
<https://docs.opencloud.ai/sdk/javascript/telemetry> for the exact response.

Define custom metrics only when they express a product or workflow signal the
platform cannot derive. Keep the catalog small and bounded. Never use user IDs,
emails, URLs, object keys, or arbitrary strings as dimensions.

Connect every custom repair signal end to end: declare the counter or gauge,
declare its matching `observability.alertRules` threshold in the same manifest,
and emit the measurement through the supported browser SDK at the real product
failure boundary. Use counters with `sum` for occurrences and gauges with
`latest` or `max` for stuck state or backlog. Do not invent a telemetry member
in the Function context, and do not add a fake failure solely to exercise an
alert example.

```js
await opencloud.telemetry.increment("items_created", 1, {
  dimensions: {
    actor_type: "member",
  },
});
await opencloud.telemetry.gauge("overdue_items", overdueCount);
```

Record a real browser-observed failure without replacing the original error:

```js
async function summarizeItem(itemId) {
  try {
    return await opencloud.functions.call("summarize", { itemId });
  } catch (error) {
    try {
      await opencloud.telemetry.increment("item_summarization_failures");
    } catch {
      // Keep the product failure as the primary error.
    }
    throw error;
  }
}
```

The SDK creates the private idempotency key and safely retries one transient
write. Application code supplies only the metric value and bounded declared
dimensions. Browser measurements are product signals, not trusted security
evidence.

For retained production notification history, use
`opencloud app notifications list <app-id>` and
`opencloud app notifications get <app-id> <message-id>`.
Use `opencloud operation list <app-id> --page` for paginated durable operation
history; continue with `--cursor` and the same filters and limit. Read command
help for supported filters. These reads use the existing exact-app session.

CLI `v3.18.0` provides `alert-rule` and `agent-feed` for operationally
configuring and reading these signals. For important app-owned failure modes,
define the bounded custom metric and its matching threshold under
`observability.alertRules` in the same manifest revision. Manifest rules are
validated before promotion and become active atomically with the deployment;
do not require a post-deployment rule-creation step for intended app behavior.
Use `alert-rule put` for an immediate operational rule or override against a
metric in the active deployment. If that rule becomes durable product intent,
move it into the next manifest revision and remove the operational override.
Do not bypass the protected credential store. Prefer the Agent Feed over raw
logs or metrics. Background queues contribute
`jobs.created`, `jobs.retried`, `jobs.succeeded`, `jobs.failed`, and
`jobs.active` signals, safe failure events, and a built-in failure alert. On
full MCP, use `list_background_jobs` and `get_background_job` for retained
per-queue and individual metadata. `list_background_jobs` accepts inclusive
ISO 8601 `from` and `to` creation-time bounds and a returned `nextCursor` for
pagination; time bounds apply to totals, queue rollups, and history. Payloads,
idempotency keys, cancellation, and redrive are intentionally unavailable. Alerts
inform the agent. An Agent may create an app-scoped custom-metric rule; each
threshold occurrence creates one deduplicated fire in the same conversation,
which stays unresolved until an exact-run repair deployment is verified.
An alert never widens a credential. An exact-app owner Agent already has
authority to choose rollback, deletion, or another owner operation for its
assigned app without another approval; narrower and legacy principals keep
their existing restrictions. Shared-platform repair always remains outside the
app boundary. See the telemetry reference for the exact contract.

## Validate, deploy, and verify the real UI

Test in the development preview, not on the live app. If you must add
something to the live app to check it, give it an obvious test name, remove it
through the app afterwards, and tell the owner about anything you could not
remove.

Installations may enable online app updates when SQL, SDK and runtime
declarations are unchanged and deployment health is static. The old release
serves during preparation; started Functions retain their captured release
within their deadline, subject to current access and suspension. Inspect the
deployment's `deploymentMode` and `preparationPhase`. SQL or capability changes,
initial deployment and Function-backed health use maintenance mode. Keep lazy
assets content-hashed and include `.vite/manifest.json` (`build.manifest: true`)
for retention across compatible deployments; copied public files are not
retained. Handle expired assets by reloading. See the
[deployment guide](https://docs.opencloud.ai/guides/deployments).

With MCP, call `validate_draft`, then:

1. `start_dev_session` for the exact validated revision; when the user asks to
   review the app before deployment, give them the returned
   `browserPreviewUrl` rather than the raw `previewUrl`. The browser entry
   requires an owner or builder login, then opens a clearly marked **Not live**
   development-preview window with Full size, Tablet, Mobile, and Reload tools.
   Its framed app uses isolated synthetic user A without replacing the user's
   normal OpenCloud session. An explicit no-deploy request stops at this review
   point and leaves the dev session active; it does not authorize promotion;
2. `apply_dev_revision` after every later coherent validated change;
3. `request_dev_app` to inspect the preview and REST reads;
4. Use the separate fixture tools only for isolated dummy data:
   `create_dev_data_row` takes `table` and one `values` object;
   `create_dev_data_rows` takes `table` and 1–100 `values` objects;
   `update_dev_data_row` takes `table`, `id`, and `values`;
   `delete_dev_data_row` takes `table` and `id`. Each also requires the exact
   `appId` and `sessionId`, authenticates as synthetic user A, and preserves
   that identity through browser verification. Never pass a REST path, action
   selector, or user credential;
5. exercise each Function through its intended path: visible app UI for
   ordinary Functions, enqueue through a producer for queue consumers, and
   synthetic injection for inbound-email handlers; then use
   `list_dev_invocations` for correlated diagnostics. Direct Function and cron
   invocation are CLI/API operations, outside the MCP catalog. Development runs
   no schedules, so verification only starts a Function that only a schedule
   calls. Never add another trigger, such as a public address, just to run it;
6. for app email, inspect `list_dev_email_captures` and
   `get_dev_email_capture`, then exercise every receive-capable alias with
   `inject_dev_email` and a reserved `.test` sender;
7. for Web Push, invoke its sending Function and inspect
   `list_dev_notification_captures` for the expected synthetic user, title,
   body, resolved icon, and same-origin click path; remember that iOS/iPadOS
   presents the installed app icon rather than the captured payload icon;
8. `verify_dev_session` for the exact active revision;
9. `promote_dev_revision` for that receipt;
10. `get_operation` until the durable deployment is terminal;
11. `verify_app`, its operation, and `get_verification_run` until every required
    production gate is terminal; and
12. `get_app` to confirm the active deployment and canonical HTTPS URL.

Make at most ten `verify_dev_session` attempts for one build. Use validation,
preview reads, and explicit Function evidence between attempts; after a tenth
failure, stop and report that structured result instead of looping.

With the CLI, run app-local syntax checks/tests/build plus:

```bash
opencloud validate "$APP_DIR"
```

Start the isolated development loop before changing production:

```bash
opencloud app dev start "$APP_DIR"
# after each coherent edit batch
opencloud app dev sync "$APP_DIR"
opencloud app dev request "$APP_DIR" /
```

The `start` result includes `session.browserPreviewUrl`, and `status` prints the
same value as top-level `browserPreviewUrl`. Give that URL to a human reviewer,
never the raw `previewUrl`. It opens the marked development-preview window and
its Full size, Tablet, Mobile, and Reload tools. If the user asked not to
deploy, leave the dev session running for review and do not run `app dev
promote`.

The capability URL has a separate migration-replayed schema. Auth, data,
managed Files, Functions, and background jobs are isolated from production; generated secrets
receive synthetic values. Owner-configured secrets, Realtime, runtime
telemetry, and cron are unavailable. For external E2E verification, each
`REQ-###` test gets a child sandbox with a fresh schema, managed Files
namespace, Function namespace, and three short-lived synthetic browser
sessions: owner A, admitted member B, and unrelated user C. The platform drops
those resources after the test even when the app's own UI cleanup fails.
Functions execute only when the CLI or a deliberate preview interaction calls
them.
Frontend-only syncs preserve dev data. A migration definition change resets
the dev schema and replays the complete ordered history. Treat every listed
unavailable capability as unavailable; never fall back to production.

Create isolated dummy fixtures without touching production:

```bash
opencloud app dev data "$APP_DIR" items create \
  --values '{"title":"Preview item"}'
```

For an email-capable app, exercise its dev-only mailbox without contacting the
production provider:

```bash
opencloud app dev email inject "$APP_DIR" \
  --to support --from customer@example.test \
  --subject "Test request" --text "Please acknowledge this message."
opencloud app dev email list "$APP_DIR"
opencloud app dev email get "$APP_DIR" "$MESSAGE_ID"
```

For a Web Push app, invoke its sending Function and inspect the captured
payload without contacting a browser push provider:

```bash
opencloud app dev notifications list "$APP_DIR"
```

Run the exact-revision verification and promote its receipt:

```bash
opencloud app dev verify "$APP_DIR" \
  --idempotency-key "$VERIFY_IDEMPOTENCY_KEY"
opencloud app dev promote "$APP_DIR" \
  --idempotency-key "$PROMOTE_IDEMPOTENCY_KEY"
```

Any source, migration, or production-base change invalidates promotion. Direct
`deploy` remains a lower-level automation path but is not the default agent
workflow because it has no dev verification receipt.

`promote_dev_revision` (or CLI `app dev promote`) is the default completion
path. Follow the durable deployment, run authoritative feature-aware production
verification, report the live URL, and stop dev only after success. The copied
user prompt authorizes promotion of the exact verified receipt for the general
builder flow, while an exact-app owner Agent session independently carries
standing authority for every same-app deployment and operation. Neither flow
adds another confirmation. Dev is an iteration environment, not a finished
result.

MCP `verify_app` (or CLI `app verify`) remains available as a standalone
durable release gate. It checks that the recorded deployment is still active,
re-hashes its immutable artifact and manifest, matches the pinned OpenCloud SDK,
and sends only a canonical `HEAD`. It does not load the production document,
run Chromium or `tests/opencloud.e2e.js`, create users, or access application
data; it writes only durable control-plane verification metadata. If a newer
deployment became active, verification refuses instead of following the moving
pointer. The full product gate is the exact-revision development receipt; CLI
v3 provides no partial local substitute for that.

Every new app must replace the starter's failing external spec with tests whose
titles begin with stable `REQ-###` IDs. Tests import exactly `test` and `expect`
from `@opencloud/test`, drive visible accessible controls, assert exact durable
create/reload/update/filter/navigation/delete outcomes where applicable, and
clean unique fixtures through the UI. Each test needs at least one trusted
action, such as a click, fill, select or reload, and one trusted assertion, or
it fails as `E2E_TEST_TOO_SHALLOW`; a test that only checks what a page shows
can reload it first. The bounded runner supplies owner, admitted
member, second-owner-tab, unrelated, and signed-out visitor pages plus
deterministic file fixtures; it rejects skip/only, direct network/backend
access, evaluate, routing, direct navigation, and script injection. Test code
runs in a blank network-isolated controller and cannot read app globals,
cookies, SDK clients, or response bodies. The supplied `marker` is short and
unique to the current REQ test rather than shared with the other tests in the
run. Use `uniqueValue(prefix, maximumLength)` for bounded fields so the unique
suffix cannot be truncated (`maximumLength` must be at least the marker's
length, about 30 characters), `clickIfVisible(locator)` only for genuinely
optional cleanup controls, and `reloadUntilVisible(locator)` for a result that
a queue saves after another page's confirmation. Upload `files.tinyPng`,
`files.tinyPdf` or `files.tinyText` with `setInputFiles`, or write a small
text file yourself with `files.text(name, contents)`, such as
`files.text("sales.csv", "date,total\n2026-10-01,12.50\n")`, up to 64 KiB.
Test code doesn't run in Node, so `Buffer`, `fs` and `process` don't exist.
`memberPage`, `unrelatedPage` and `anonymousPage` may see expected 401 and
403 responses, such as when a test checks that an owner-only screen stays
closed to them. A locator that matches several elements fails, for example
a product name that appears in both a table and a chart: narrow it with a role
and name, `filter({ hasText })`, or the region that holds it.

The owner, member, and unrelated pages can all load only the isolated
development app. `unrelatedPage` is a distinct authenticated identity with no
implicit app-authored ownership or team membership; use it to challenge owner-
or team-scoped business RLS. Establish any app-domain membership through visible
UI. Do not expect `unrelatedPage` to be denied when the app intentionally shares
records with every admitted user through a permissive `using (true)` policy.
Private production verification separately requires the anonymous canonical
`HEAD` to redirect to Auth.

`anonymousPage` is a signed-out visitor: its own browser context with no
session, as someone without an account sees a public app. Use it for every
journey that such visitors take, such as a public sign-up, order or booking:
act as the visitor, assert what the visitor sees, then check the result on
`ownerPage`. A queue saves the submission moments after the visitor sees the
confirmation, so wait for it before asserting it:

```js
test("REQ-002 a visitor books a place", async ({ anonymousPage, ownerPage, uniqueValue, reloadUntilVisible }) => {
  const name = uniqueValue("Visitor", 60);
  await anonymousPage.getByLabel("Your name").fill(name);
  await anonymousPage.getByRole("button", { name: "Book my place" }).click();
  await expect(anonymousPage.getByRole("status")).toContainText("You're booked");
  await reloadUntilVisible(ownerPage.getByText(name));
  await expect(ownerPage.getByText(name)).toBeVisible();
});
```

`reloadUntilVisible(locator)` reloads that locator's page about once a second,
for up to about 15 seconds, and returns whether it appeared. A reload keeps only
what the page's address holds, so wait for a result the reloaded page shows
without another click. Development hosts admit signed-out visitors even to a
private app, so the receipt's
`signed-out-visitor` phase says whether the app was public when the tests ran.
A live private app sends signed-out visitors to sign in: make it public before
you publish. For the same reason, don't write a test that expects a private app
to turn a signed-out visitor away; verification of the live app checks that
itself. When a submission takes a place or a slot, assert that the page shows
it taken straight away, without a reload.

Each REQ test starts with fresh browser contexts and an independent runtime
sandbox. The scheduler runs up to five tests concurrently by default; the
verification request may set `parallelism` from 1 through 10. Role names accept bounded
regular expressions for accessible names, label/text/test-ID/placeholder
queries, and `filter({ hasText })`; bounded scalar matchers may supplement query checks,
but scalar checks do not replace the required page or locator assertion. Scope
duplicate labels through their dialog, form, or card. In `finally`, close open
overlays and wait for cleanup state to converge before interacting with a page
that another user changed. If cleanup also fails, the verifier preserves the
first primary command error and adds bounded browser/network context. A failed
step names its page and locator, such as
`memberPage.getByText("1", { exact: true })`, says how many elements matched
it, and quotes that page's own alert and status messages, so check the locator
before assuming the data is missing. When two or more elements match, scope the
locator to the card or row that holds the test's own entry, found by its
`uniqueValue`.
`selectOption` accepts a raw value and the bounded Playwright `{ value }`,
`{ label }`, and `{ index }` forms.

For controls created only after an interaction, assert the rendered phone target
with `expect(locator).toHaveMinimumSize({ width: 44, height: 44 })`. This catches
native checkbox and similar sizing defects that cannot be proven by static source
inspection alone.

Verification success receipts and structured failures include an attempt
report with revision/artifact/test hashes, requested parallelism, outcome,
failure classification, and phase timings. Per-test evidence separates sandbox
setup, browser execution, cleanup, and total time. If another request is still
verifying the same dev session, wait after `DEV_VERIFICATION_IN_PROGRESS`; the
platform preserves the active attempt instead of starting overlapping sandbox
cleanup.

Development verification independently fails primary-flow HTTP 404/405, mobile horizontal
overflow, unnamed visible controls, primary touch targets below 44 by 44
pixels, an element marked `hidden` that page styling still shows, and an urgent
error message (`role="alert"` with `aria-live="assertive"`) on the first
screen; use `role="status"` for notices that are not errors. A test also fails
when it ends with a page still showing an element marked `hidden`. Start every
stylesheet with `[hidden] { display: none !important; }`, because a class that
sets `display` otherwise keeps showing what the code hid, such as a loading
placeholder over a photo. Wrap an action whose request should fail with a 4xx, such as a rejected
sign-in, in `expectResponseStatus(page, path, status, action)`; verification
then accepts the browser's error for exactly that response. Its private-app run uses short-lived admitted synthetic users and
removes their sessions, grants, and identities. Production retains only the
anonymous Auth `HEAD` redirect check.

Run every UI assertion in development. Managed Files, Functions, and background
jobs are available inside each disposable E2E
sandbox and use isolated namespaces on the ordinary capability URL. Realtime
and cron remain unavailable in dev and must
never fall back to production. There is no production E2E run. Realtime remains
best-effort and cannot block CRUD or cleanup; `opencloud.data` is the durable
source of truth.

Inspect deployments, error logs, usage, and cron history after normal traffic.

## Run manifest-derived release verification

The schema 2 or 3 manifest and immutable `tests/opencloud.e2e.js` artifact are the
development-verification inputs. Use the existing `@opencloud/test` API rather than
creating another test DSL or a local partial-verification configuration.

```bash
opencloud app verify "$APP_ID"
```

The durable production operation checks the recorded active release, exact
artifact digest and manifest, SDK metadata, and canonical `HEAD`. It does not
execute the external E2E specification. Use the isolated dev tools to exercise
Data, Files, Auth, and every declared Function before promotion; inspect cron
history, logs, and usage after normal production traffic.

Development verification counts a Function as exercised once its own handler
answers with a status below 500. A deliberate 4xx counts, such as a webhook
rejecting a missing signature, since development never receives owner
secrets; never weaken an auth check to produce a 2xx. A Function still fails
`DEV_FUNCTION_COVERAGE_FAILED` if it only returned server errors, got no
response, was rejected by OpenCloud before its handler ran (admission, access
checks or SDK input validation), or was never invoked.

## Apply the current authority boundary to destructive operations

Create and inspect backups before risky work when appropriate. Do not roll
back or restore merely to test a deployment. Code rollback replaces runtime
code/config but does not reverse migrations; database restore can discard
newer writes.

An exact-app owner Agent may delete, archive, stop, roll back, restore, mutate
production data or Files, rotate or delete secrets, and change access for its
assigned app without requesting another per-operation approval. Prefer
idempotent, recoverable commands, inspect the terminal operation, and verify
the resulting state. The app is the security boundary, not a reason to stop.

For ordinary human-directed sessions and legacy or narrower Agent credentials,
keep the prior rule: do not perform a destructive action unless the user
clearly authorized that exact action. No credential permits cross-app,
account, platform, host, or shared-infrastructure actions, and an explicit
user instruction not to deploy or mutate remains binding.

## Report completion honestly

For online work, record canonical URLs, IDs, exact validation/UI/verifier/log/
usage/cron outcomes, deployment state, observed friction, product limitations,
and confirmation that the app remains active—or the exact terminal deletion
receipt when deletion was the intended result.

For offline work, report the artifact digest and exact remaining online steps.
An offline-valid bundle is useful progress, not a deployed app.