Skip to content

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

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 workspace.
  3. Otherwise run opencloud auth status. Reuse a healthy account login or run opencloud login and let the user approve it in their browser. 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. Call get_opencloud_session.
  2. If it reports disconnected, call connect_opencloud for a returning user, or continue with start_onboarding for first registration.
  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. After confirming their email, the owner can build personally. Company creation is optional and lives in Preferences; 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 explains that approval is required to continue using it. 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. Existing provisional_ready grants remain usable; if their verification.emailSent is false, report the delay and continue the build.
  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.
  • 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:

FailureCorrect recovery
draft revision or file hash conflictRe-read draft/files and reapply only the intended change.
manifest diagnosticFix the reported source path; use get_app_starter when the manifest or SDK pattern is missing.
development migration failureFix the named migration file/SQLSTATE, validate a new revision, and apply it.
Chromium certificate failureTreat it as an environment TLS failure; never disable certificate validation.
expired/stale receiptReapply the exact current revision, repeat dev checks, and obtain a new receipt.
durable operation failureReport 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.10.3 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.10.3"

CLI 3.10.3 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.

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.10.3. 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:

bash
opencloud auth status
opencloud login
opencloud app list

# Only when the requested app does not already exist:
opencloud app create --name "$PROJECT_NAME" --visibility private

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

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

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.

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" \
  --version cli-v3-compat

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

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/header/cookie access. 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.10.3 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. 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:

InterfaceMethods
VersionOPEN_CLOUD_SDK_VERSION
Appapp.info()
Authauth.currentUser(), auth.requireUser(), auth.signInUrl()
Datadata.table(name).list/getById/create/createMany/updateById/deleteById
Filesfiles.upload/info/download/save/replace/remove/attach
Functionsfunctions.call(name, input?), functions.stream(name, input?) for user/public Functions
Notificationsnotifications.status(), notifications.subscribe(), notifications.unsubscribe()
Realtimerealtime.subscribe(topic, handler), realtime.publish(topic, event, payload)
Telemetrytelemetry.summary/increment/gauge
Clientdispose()

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" });

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.

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:

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.

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. These are high-level, invocation-scoped capabilities; raw database clients, buckets, provider keys, and 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.

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 bounded idempotency key. 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, or inbound email consumer may access. 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. 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 explicit invoke_dev_function MCP call, app dev invoke CLI command, or deliberate preview interaction 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.

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.

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.

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.

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.

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 uses the mode the owner chooses in App Integrations. The default App-provided AI mode needs the owner to select an AI connection for that app; the connection may be a Codex/ChatGPT device login or an OpenAI API key. If an invocation reports app_ai_credential_not_selected, give the owner the existing non-secret launch URL once and resume after that explicit consent action; never substitute or copy the coding agent's credential. 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 in App Integrations.

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

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. mutate_dev_data only for isolated dummy fixtures, using the high-level table, action, values, and optional id fields. It authenticates as synthetic user A and preserves that identity through browser verification; never pass a REST path or user credential;
  5. exercise each Function through its intended path: invoke_dev_function 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;
  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 and one trusted assertion. The bounded runner supplies owner, admitted member, second-owner-tab, and unrelated 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, and clickIfVisible(locator) only for genuinely optional cleanup controls.

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.

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/status context. 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, and primary touch targets below 44 by 44 pixels. 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.

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.

Self-hosted infrastructure for agent-built applications.