npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

wrapflow

v0.3.74

Published

Wrapflow SDK, wf CLI, and MCP tools for building, inspecting, connecting, and safely releasing one typed App.

Readme

wf — build one complete Wrapflow App

An App is Flows written in the Wrapflow flow language plus an uploaded frontend.

Read with describe kind:"flow" → edit the text → apply {text, expectedRevision} → test → publish. Every command declares fields, including fields: [] for no inputs. Delete requires [] and takes only recordId/key plus expectedVersion where used; create/patch list their inputs explicitly. Use the returned updatedAt as expectedRevision; keep exported layout and metadata. Publish uses the existing owner approval. A text field holds up to 500,000 characters in one value (max: sets its limit in flow text, maxLength in define JSON; the default is 2,000), so write long text whole and never split it across fields. Above 10,000 characters a field is long text: lists and queries omit it unless select names it, and it cannot be unique, filtered, sorted or searched. One table's long text fields declare at most 500,000 characters together.

Example:

let habits = table(name: "Habits", fields: [text(name: habit_name, required: true, max: 80)])
let entries = table(name: "Entries", fields: [text(name: habit_name), number(name: score)])
let positive = filter(input: entries, where: score > 0)
let kept = aggregate(input: positive, by: [habit_name], total: sum(score))
let joined = join(left: habits, right: kept, type: left, on: habit_name, take: [total], rightDedupe: none)
let result = show_table(input: joined, columns: [habit_name, total], limit: 100)

The ordinary wf surface is four commands, one App journey:

npm install --save-dev --save-exact --ignore-scripts 'wrapflow@<reviewed-version>'
# Invoke only the exact reviewed project-local package. Do not ask npx to
# resolve `wf`: when this dependency is absent, it can select an unrelated package.

node ./node_modules/wrapflow/wf.mjs login --url https://<host> --email [email protected]  # securely prompts for password
node ./node_modules/wrapflow/wf.mjs app create "Client request portal" \
  --intent "Build a client request portal where clients submit a request with title, details, priority, and due date; operators can review status and ownership. Include safe demo requests clearly marked as demo data." \
  --request-id build-client-request-portal-2026-07-17 \
  --host self
node ./node_modules/wrapflow/wf.mjs app preview <app-id>   # private draft URL
node ./node_modules/wrapflow/wf.mjs app publish <app-id>   # readiness → proof → release
node ./node_modules/wrapflow/wf.mjs app status <app-id>    # live hosting state

wf app create makes the hosting decision exactly once: --host wrapflow ("Host my App with Wrapflow") or --host self ("Connect my own frontend"). An interactive terminal is asked the one question; a non-interactive --intent build infers self. No later command asks again. wf app publish drives the full release authority path — readiness and exposure review, then isolated browser checks (--checks --reviewed-exposure), then the exact approved authority document (--authority release-authority.json, admin token). Nothing about check or the operator boundary is skippable.

Everything else lives under explicit namespaces: wf app data … (records in and out), wf app integration … (endpoints, typed clients, connectors, MCP), wf app operations … (runs, results, reviews, access, incidents, hosted-backend traffic), wf dataset … (saved datasets shared across Apps — wf dataset delete <id> --yes, admin token), and wf advanced …. Run each with --help for its commands.

Old command spellings

Every pre-existing spelling keeps working as a compatibility alias until 2026-11-01 and prints one migration line on stderr naming its replacement:

| Old | New | | --- | --- | | wf app build | wf app create --intent "…" | | wf app list | wf app status | | wf app authority / wf app checks / wf app release | wf app publish (--checks, --authority) | | wf app inspect/platform/access/user/incident/fixtures | wf app operations <same> | | wf app connector/client/contract | wf app integration <same> | | wf app attach/edit/get/save | wf advanced <same> | | wf publish (share URLs) | wf app integration share | | wf api/stream/run-api/scope/invoke/job/mcp | wf app integration <same> | | wf data | wf app data snapshot | | wf query/mutate/transact/submit/call/log/import/export/backup/restore/reset | wf app data <same> | | wf run/runs/reviews/review/result/latest/open | wf app operations <same> | | wf list/create/release/release-review/define/undefine/remove/describe/test | wf advanced <same> |

The sections below document the underlying operations; their examples use the historical spellings, which remain exact aliases of the commands above.

Inspect and integrate a draft

node ./node_modules/wrapflow/wf.mjs app inspect <app-id>
node ./node_modules/wrapflow/wf.mjs app contract <app-id>
node ./node_modules/wrapflow/wf.mjs app client <app-id> --file src/wrapflow.generated.ts
node ./node_modules/wrapflow/wf.mjs app edit <app-id> \
  "Add client-visible status history and a weekly overdue queue" \
  --revision <current-frontend-revision> \
  --frontend external

Replace <reviewed-version> with one exact reviewed version and commit the lockfile. Do not resolve the CLI from latest or another mutable tag.

Keep the App ID and frontendLane, frontendRevision, and frontendUpdatedAt returned by inspect. This quickstart intentionally stops at a private draft. Continue to the guarded complete App lifecycle below only when its authority and publishing preflight are available.

Deterministic authoring: no AI call, no AI credits

app build and app edit call a model. When the product structure is already known, or the entity and command schemas must be exact rather than inferred, author the same App deterministically instead. None of these commands spend AI credits:

For a single typed object, create the App container and initialize its own backend before defining it. For whole-Flow changes, use the text path above:

APP_ID=$(node ./node_modules/wrapflow/wf.mjs app create "Client request portal" --frontend external)
# stdout stays the pipeable App id; stderr confirms external lane revision 1.
BEHAVIOR_ID=$(node ./node_modules/wrapflow/wf.mjs create --empty --name "Requests")
node ./node_modules/wrapflow/wf.mjs app attach "$APP_ID" "$BEHAVIOR_ID"
# Attach the still-blank behavior before define makes its graph App-aware.

node ./node_modules/wrapflow/wf.mjs describe "$BEHAVIOR_ID" entity            # exact JSON schema + current definitions + updatedAt token
node ./node_modules/wrapflow/wf.mjs test "$BEHAVIOR_ID" entity '{"spec":{…}}' # preflight a candidate spec; mutates nothing
node ./node_modules/wrapflow/wf.mjs define "$BEHAVIOR_ID" entity --file entity.json \
  --expected-updated-at <token-from-describe>                            # apply one business-level object
node ./node_modules/wrapflow/wf.mjs define "$BEHAVIOR_ID" command --file command.json
node ./node_modules/wrapflow/wf.mjs describe "$BEHAVIOR_ID" view              # inspect declared views and JSON schema
node ./node_modules/wrapflow/wf.mjs define "$BEHAVIOR_ID" view --file view.json # define server-side computed/aggregate view
node ./node_modules/wrapflow/wf.mjs test "$BEHAVIOR_ID" view '{"name":"status-totals","context":{"entities":[{"name":"orders","rows":[{"item":"A","status":"shipped","amount":50}]}]}}' # test view evaluation with ephemeral rows
node ./node_modules/wrapflow/wf.mjs query --flow "$BEHAVIOR_ID" --view status-totals # query declared view

# Make this behavior start when rows land in an App table and write to another (a chain; see docs/BUILD-WITH-WRAPFLOW-AS-AN-AGENT.md)
node ./node_modules/wrapflow/wf.mjs define "$BEHAVIOR_ID" chain '{"name":"refine","from":"raw","to":"refined"}'
node ./node_modules/wrapflow/wf.mjs describe "$BEHAVIOR_ID" chain            # "Refine runs when rows land in raw and writes to refined."

# Remove obsolete actions, entities, views or chains from a draft
node ./node_modules/wrapflow/wf.mjs describe "$BEHAVIOR_ID" command
node ./node_modules/wrapflow/wf.mjs undefine "$BEHAVIOR_ID" command obsolete-action \
  --expected-updated-at <updatedAt-from-describe>                       # remove declared command from draft
node ./node_modules/wrapflow/wf.mjs describe "$BEHAVIOR_ID" entity
node ./node_modules/wrapflow/wf.mjs remove "$BEHAVIOR_ID" entity temp-table \
  --expected-updated-at <updatedAt-from-describe>                       # remove declared entity (refused if referenced)
node ./node_modules/wrapflow/wf.mjs remove "$STRAY_BEHAVIOR_ID"                     # delete an owned stray standalone draft background app

The stable kinds are entity (named durable collection with typed fields, relationships, and form/table/edit operations), command (named atomic write with typed input and guards) and view (named server-run read over one entity: fixed where clauses, which may compare two fields of the same row such as stock_level lte reorder_threshold, compare a date field to today ("relativeTo":"today", UTC, read on every read), or filter on a field of the related row one reference hop away ({"relation":"company","field":"customer_tier", "op":"eq","value":"gold"}; two hops are refused), plus columns or counted/summed aggregate groups; pages read it with POST /api/public/flow/<token>/query {"view":"<name>"}, recipe in docs/BUILD-WITH-WRAPFLOW-AS-AN-AGENT.md) and rule (a named backend rule that something is NOT happening: a record's date has passed while where clauses still match, a record was not updated for a duration, or a table got no new row for a duration; the server checks it about every 5 minutes in UTC, alerts the owner once per record or table, clears when it stops being true, and describe lists each rule in plain words with its open incidents). To share a table with another App in the account (by reference, no copy), list that App's id in the entity's readers and in the grants of each command it may run. Draft sharing uses the source's preview rows before either App is released. Define warns that public access requires releasing the source with the grants. After that release, read the reader's App Contract and generate its client: shared reads and commands appear with sourceApp.id and use ordinary generated getters on the reader's own routes. No records are copied and no direct table writes are granted. The worked workshop/reception example in docs/BUILD-WITH-WRAPFLOW-AS-AN-AGENT.md includes both calls and the retained public routes. Always describe first for the exact schema of a kind, test a candidate before applying it, and pass the updatedAt token from wf describe <app-or-behavior-id> <kind> as --expected-updated-at to wf define or wf undefine. When using a token, redefining or removing an existing object requires that flag; omitting it returns 428 definition_revision_required. First-time definitions do not require it. Read the current definition and its revision before each change so a person's newer edit cannot be overwritten silently. The CLI forwards the revision you supply and never fetches it for you. The two payloads are deliberately not the same shape: test takes a scenario, so a candidate definition must be wrapped as {"spec":{…}}, while define takes the bare spec, so entity.json or view.json holds the spec object itself with no spec wrapper. A command can take an amount from one record of another entity in the same save — an order taking qty from inventory.stock_level, refused with your message when less than 0 would be left — with a take guard:

{"name":"place-order","entity":"orders","guards":[{"source":"inventory",
  "where":[{"field":"_wf_record_id","op":"eq","valueField":"product"}],
  "column":"stock_level","take":{"amountField":"qty"},"message":"Not enough stock for this order."}]}

An order with several lines carries one take guard per line (product_1/qty_1, product_2/qty_2, ...): all lines are taken or none. Without a message the refusal names the short item.

A computed aggregate view defines server-calculated sums, counts, and groups:

{
  "name": "status-totals",
  "entity": "orders",
  "aggregate": {
    "groupBy": ["status"],
    "metrics": [
      { "op": "count" },
      { "op": "sum", "field": "amount" }
    ]
  }
}

test on that command reports relatedChanges (stock_level from → to) without writing. test on a view evaluates the view over current rows (or ephemeral context rows) and returns { rows, total, groups }. The full recipe is in docs/BUILD-WITH-WRAPFLOW-AS-AN-AGENT.md. Node ids and storage keys inside the behavior never cross this boundary — these verbs are the supported deterministic interface, unlike raw graph JSON. MCP exposes the matching create_app, attach_behavior, define, describe, and test tools; pass frontendLane: "external" to create_app for an App-owned frontend. The SDK exposes createApp, createEmpty, define, describe, and test; call wf.createApp({ name, frontendLane: 'external' }) for the same App container and external authority, and use wf.request('/api/apps/<app-id>/flows/<behavior-id>', { method: 'PUT' }) for the historical attachment route. Omitting the lane on any deterministic creation surface preserves the empty container with no frontend authority.

Test a draft as someone not signed in

MCP query_entity, mutate_entity, and call_command accept appId with draftAs: "anonymous" instead of tokenOrUrl. For example:

{"appId":"app-1","draftAs":"anonymous","entity":"tasks","limit":20}

This calls the current draft with account authentication for draft access and x-wrapflow-draft-as: anonymous for the visitor's permissions. It sends no App-user session and needs no sign-in setup. Pass pipelineId when the App has multiple Flows. Queries use JSON; CSV queries require a published capability. An ownershipToken can identify a returning anonymous submitter. Server access refusals are returned unchanged. Do not combine the draft target with tokenOrUrl or appUserToken.

The SDK exposes the same path as wf.callAsAnonymous(appId, {flowId, entity, action: 'query' | 'mutate', input}) or {flowId, command, input}; input is the runtime request body. A wfd_ value identifies a draft revision for release checks; it is not a capability.

define behaves differently on an App-owned behavior

define is App-aware. The command, the spec, and the returned definition are identical either way, but the persisted behavior graph is not:

  • On an App-owned behavior it keeps only backend nodes — source.collection and output.edits. The App-owned frontend presents them through the typed App Contract.
  • On a standalone background app it additionally keeps the retained canvas presentation nodes output.input and output.table, so the app renders on its own /f/ view.

That difference decides the command order above. wf create --empty initially makes a standalone background app, but its blank graph is eligible for wf app attach. Attach it before the first define; defining an entity while it is still standalone writes output.input and output.table, and a later attachment is rejected with 409 app_backend_presentation_forbidden. No supported verb strips those presentation nodes.

The two App behavior routes are not the same route

| Route | What it does | | --- | --- | | POST /api/apps/<app-id>/flows | Creates a new blank behavior already owned by the App. Body {name?, description?}, returns the behavior with status 201. This alternative direct starting point is reachable through the SDK request escape hatch. The route name is historical. | | PUT /api/apps/<app-id>/flows/<flow-id> | Attaches an existing background app as a behavior, keeping its ID, runs, releases, and compatible endpoints. wf app attach <app-id> <behavior-id> and MCP attach_behavior wrap it. Returns {ok:true,appId,flowId}. It rejects a behavior graph containing presentation nodes. The route and response field names are historical. |

The create route still has no dedicated CLI or MCP verb; the deterministic sequence above creates a blank standalone behavior, attaches it, and only then defines it. Attachment requires draft scope and Preview authority. Attaching to an inaccessible App is refused as 404 app_not_found. Reattaching an otherwise eligible behavior already owned by the same App succeeds idempotently: the server reapplies its disabled tokenless App policy and returns the same App/behavior identity, though the behavior update timestamp advances.

Release is unchanged: a definition only edits the draft, so run and release the behavior (or take the App through its guarded lifecycle below) afterwards.

Advanced: background apps and recovery

Use the lower-level background app path only for a standalone transformation, report, scheduled job, or recovery operation rather than a complete App:

node ./node_modules/wrapflow/wf.mjs list
node ./node_modules/wrapflow/wf.mjs create "log my gym sessions: exercise, sets, reps, weight"
RUN_ID=$(node ./node_modules/wrapflow/wf.mjs run <id>)            # defaults to draft; exact successful draft run id on stdout
node ./node_modules/wrapflow/wf.mjs release-review <id>          # defaults to latest successful draft run (or pass --from-run)
node ./node_modules/wrapflow/wf.mjs release <id>                 # atomically seals the latest draft run into the LIVE release (or pass --from-run)
node ./node_modules/wrapflow/wf.mjs schedule <id> --cron "0 2 * * *" [--tz UTC] # recurring schedule (or --disable)
node ./node_modules/wrapflow/wf.mjs schedule <id>                         # read the current schedule
node ./node_modules/wrapflow/wf.mjs stream <id> --as gym                # webhook intake on, alias saved
node ./node_modules/wrapflow/wf.mjs log gym exercise=squat sets:=5 reps:=5 weight:=100
node ./node_modules/wrapflow/wf.mjs run <id> --target live              # or: cat fresh.csv | node ./node_modules/wrapflow/wf.mjs run <id> --target live
node ./node_modules/wrapflow/wf.mjs run-api <behavior-id> --input <file-or-sample-step-id> # admin: save authenticated request-body target
node ./node_modules/wrapflow/wf.mjs result <run-id> --csv
node ./node_modules/wrapflow/wf.mjs latest <id> --csv                   # newest readable result of the live release
node ./node_modules/wrapflow/wf.mjs run <id> --target live --download file > out.csv
node ./node_modules/wrapflow/wf.mjs backup <id> --dir backups/prod      # exact restorable entity state + history
node ./node_modules/wrapflow/wf.mjs restore <id> --dir backups/prod --yes
node ./node_modules/wrapflow/wf.mjs export <id> --dir backups           # every record of every entity → CSV files
node ./node_modules/wrapflow/wf.mjs import data.csv --pipeline <id> --collection <nodeId> [--replace]  # --replace needs admin-scope token
node ./node_modules/wrapflow/wf.mjs reset <id> --yes                    # DANGER: clear all records/submissions (go-live step)

wf import bulk-loads rows into one collection entity (CSV or JSON). It uses the owner bulk ingest API: typed validation, key-column upserts, optional --replace (requires an admin-scope token) to clear the entity first, and the same 5,000-row / 2 MB caps as the HTTP route.

wf run-api is the supported owner control for file-bearing authenticated Run API calls. It validates the chosen File or Sample step against the behavior's current Live release, persists that exact target, and only then prints no-body, raw CSV, raw JSON, and multipart examples. An unrelated Run API update keeps the saved target. If that step is later deleted or changes kind, Studio, the control API, and the Run API report the stale setting and ask the owner to choose and save a current target. The route's pipelineId field remains only as its historical API identifier; user-facing output calls the resource a behavior.

wf release performs the same review automatically and passes only the server-authored plan hash into the commit. SDK callers can use reviewRelease(id,{createdFromRunId}) followed by release(id,{createdFromRunId,dataMigrationPlanHash}); MCP exposes review_workflow_release and applies the same sequence in release_workflow. Constraint tightening validates every bounded live row. A violation prints the exact rejected count and bounded record/field/reason sample, leaves v1 and all rows intact, and must be remediated explicitly with wf mutate, SDK mutateEntity, or MCP mutate_entity before rerunning and reviewing. A racing write stales the plan. Default backfills and constraint checks commit atomically, and the release receipt lists both exact operations.

wf export is the portable data exit hatch: readable current-state CSV/JSON, but no tombstones, retained event history, or idempotency state. wf backup is the disaster-recovery path. It freezes each Durable Object before paging it, writes NDJSON files with row counts and SHA-256 hashes, and publishes manifest.json last so an interrupted directory is never mistaken for a complete bundle. wf restore validates the entire bundle before opening any restore session and requires --yes; each entity swaps atomically after all of its rows are staged. Apps with multiple entities commit those entities in manifest order, since Cloudflare does not provide a transaction across Durable Objects. The current and backed-up entity sets must match exactly by default; --allow-partial is an explicit escape hatch that prints every excluded entity. A private 0600 journal in the bundle records each per-entity commit. Re-run the identical command after an interruption to resume deterministically; a completed journal makes later re-runs no-ops after integrity preflight.

Advanced owner SDK

import { Wrapflow } from 'wrapflow'

const wf = new Wrapflow({ url, token: process.env.WRAPFLOW_TOKEN })
await wf.configureRunApiFileTarget(behaviorId, 'orders') // owner/admin control plane
await wf.push(streamToken, [{ exercise: 'bench', sets: 3, reps: 8, weight: 70 }])
const run = await wf.run(pipelineId, { target: 'live' })
const { table } = await wf.result(run.id)
// Or run a draft with request input (for an app with a Request step; published Request apps are invoked via their invoke-v1 entrypoint):
const draftRun = await wf.run(pipelineId, { target: 'draft', request: { city: 'London' } })

// Or let an agent/script read the newest successful result without tracking a run id.
const latest = await wf.latestResult(pipelineId)
console.log(latest.run.id, latest.table)

// Let Claude/Codex/Cursor edit the exact HTML document without buying model
// calls from Wrapflow. Compare-and-swap prevents two editors losing work.
const frontend = await wf.getAppFrontend(appId)
const saved = await wf.saveAppFrontend(appId, editedHtml, {
  expectedRevision: frontend?.revision ?? null,
})
console.log(saved.revision)

latestResult(pipelineId) asks the server for the current published version's newest readable successful result across retained history. Draft/manual results, results from older releases, and success rows whose saved result artifact has expired are never substituted. The response carries both run and version provenance alongside the canonical table. It throws an explicit WrapflowError when the app or behavior is unpublished or its published version has not succeeded yet. The CLI equivalent is wf latest <app-or-behavior-id> (pretty JSON by default, or CSV with --csv); it prints run/release provenance to stderr so stdout remains clean table data for pipes.

push() needs no API token at all — the wfh_ stream token in the URL is the credential, so a cron job, a container (viralwalk), or an IoT device can log entries with one fetch.

CLI key=value inputs stay strings so identifiers such as 00123, phone numbers, and large account numbers remain lossless. Use key:=<JSON> when a typed value is intentional: count:=5, done:=true, missing:=null, or meta:='{"source":"cli"}' (quote JSON objects for your shell). Unsafe object keys and non-finite numbers are rejected before any request is sent.

Requires Node 20.19+ (the repo's engines range). Every request times out after 30s and buffered responses are capped at 50MB; tune both with new Wrapflow({ url, token, timeoutMs, maxResponseBytes }) (timeoutMs: 0 disables the timeout).

Complete App lifecycle

The same App document used by the browser workspace is available to any coding agent through wf mcp, the SDK, or the CLI:

node ./node_modules/wrapflow/wf.mjs app build --name "Client request portal" \
  --intent "Build a client request portal where clients submit a request with title, details, priority, and due date; operators can review status and ownership. Include safe demo requests clearly marked as demo data." \
  --request-id build-client-request-portal-2026-07-17 \
  --frontend external
node ./node_modules/wrapflow/wf.mjs app inspect <app-id>
node ./node_modules/wrapflow/wf.mjs app contract <app-id>
node ./node_modules/wrapflow/wf.mjs app client <app-id> --file src/wrapflow.generated.ts
node ./node_modules/wrapflow/wf.mjs app edit <app-id> "Add client-visible status history and a weekly overdue queue" --revision 1 --frontend external
node ./node_modules/wrapflow/wf.mjs app fixtures <app-id>                     # exact proven/ambiguous data + fingerprints
node ./node_modules/wrapflow/wf.mjs app fixtures decide <app-id> \
  --request-id release-review-2026-07-12 \
  --decision clear \
  --target 'flow-id|collection-id|fixture-v2:copy-the-full-fingerprint'
# Do not run `wf app checks` before this capability preflight succeeds.
node ./node_modules/wrapflow/wf.mjs app authority <app-id> > release-preflight.json
# Stop at the draft unless proofStartAvailable is true, an authorized publishing
# path and present operator are available, and that operator reviewed the exact
# anonymous/authenticated exposure, identity, Checkout, and file capabilities.
node ./node_modules/wrapflow/wf.mjs app proof <app-id> --request-id proof-client-request-portal-2026-07-12 --reviewed-exposure
node ./node_modules/wrapflow/wf.mjs app authority <app-id> > release-review.json
# Poll authority until the exact returned proofId is eligible. Present the
# current exposure to the operator again, then construct and explicitly approve
# the exact nine-key AppReleaseRequestV2 as release-authority.json;
# release-review.json is evidence, not the request. Stop at the draft if
# authority, proof, operator review, or publishing becomes unavailable.
node ./node_modules/wrapflow/wf.mjs app release <app-id> --authority release-authority.json   # admin approval
node ./node_modules/wrapflow/wf.mjs app status <app-id>                                      # hosting check

# Low-level HTML-only escape hatches remain available:
node ./node_modules/wrapflow/wf.mjs app get <app-id> --file index.html
node ./node_modules/wrapflow/wf.mjs app save <app-id> --file index.html --revision 2

The generic SDK checkout uses the same identity protocol as wrapflow-app. Authenticated Apps pass the App-user JWT as appUserToken. Anonymous callers use an idempotency key; the first result includes an opaque ownershipToken that should be stored privately and supplied for exact replay after restart:

const checkout = await wf.checkout('wfh_app', 'subscription', {
  idempotencyKey: crypto.randomUUID(),
  appUserToken: currentAppUserJwt(), // omit for an anonymous App
})

await wf.checkout('wfh_app', 'subscription', {
  idempotencyKey: savedKey,
  ownershipToken: checkout.ownershipToken,
})

Never log either token or put it in a URL. Amount and currency remain server-declared; exact idempotency replay returns the original checkout, while conflicting reuse fails.

What wf app save accepts

--revision is a compare-and-swap token, not a version number to write. Pass --revision new for the first save, while wf app inspect still reports frontendRevision: null, then the revision the previous save returned.

wf app save is not a file upload; the server validates the document against the hosted single-file policy and rejects the whole save with 422 frontend_policy_violation, naming each bounded finding and its line when available. New hosted v5 source is arbitrary visual composition in one scriptless semantic HTML document with inline CSS. The rules that most often fail a first save:

  • A complete document is required. A fragment is rejected: the HTML needs a <!doctype html> and both <html> and </html>. Wrapflow binds the exact server-owned wrapflow-contract marker after authoring, but it never invents the surrounding document.
  • Presentation is scriptless. Every <script> element, inline event handler, executable URL, form, frame, popup, download, worker, or arbitrary network/navigation primitive is rejected. There is no allowed client script tag or inline-script exception in new hosted source.
  • Behavior is declarative. Accessible native controls and projection slots use exact Contract hooks such as data-wrapflow-action, data-wrapflow-field, data-wrapflow-result, and data-wrapflow-record-*. Each name must match the typed Contract. These hooks identify actions and data; they are not a widget, component, or style taxonomy.
  • The trusted parent owns execution. Wrapflow's manifest-pinned shell and compiler own the client, transport, authentication, action dispatch, typed validation, and bounded result rendering. The saved presentation never calls a client or constructs a route. The trusted parent shell—not the presentation—alone loads the manifest-pinned /_wrapflow/client.js; users must not add it to hosted source.
  • Resources stay bounded. Use inline CSS rather than external stylesheets or fonts. Links may target document fragments, and supported image/CSS URL positions may use only exact immutable generated App image paths. Other remote resources, srcset, and nested browsing contexts are rejected.
  • One document of at most 512 KB.

If the interface needs JavaScript, a framework, or a mature library, use the external App-owned frontend lane and integrate it with the generated AppClient instead of trying to satisfy this policy.

wf app build generates and prints the caller-owned request key before sending the request. If the response is lost or ambiguous, retry the exact same name and intent with that key via --request-id; a new product intent needs a new key. SDK buildApp and MCP build_app require an explicit 8–200 character URL-safe key and reject missing or unsafe keys before sending a request.

wf app authority reads GET /api/apps/<app-id>/release-authority, which is accessible to draft-scope tokens as well as admin tokens, allowing draft workflows to inspect release anchors and readiness. Check execution (wf app checks / POST /api/apps/<app-id>/proofs) and publishing (wf app release / POST /api/apps/<app-id>/deployments), however, require an admin-scope token; a draft-scoped token receives 403 This API token doesn't have the "admin" permission. That 403 is the expected outcome for an agent-scoped token, and the correct response to it is to stop with the App as a draft rather than to escalate.

Before running CLI checks or retrying, wf app authority must present current authority and the complete exposure to a present operator. Pass --reviewed-exposure only after that review. The check command re-reads and displays current authority and stops unless checksAvailable is still true. MCP follows the same order and requires operatorReviewedExposure=true on start_app_proof.

wf app fixtures is the supported release-time demo-data review. When it reports requiresDecision: true, release remains blocked. Copy each target's exact flowId, collectionId, and fingerprint into a repeated --target; the CLI never fetches or guesses fingerprints during a decision. --decision clear deletes only independently proven fixtures and leaves ambiguous rows untouched; --decision promote keeps the records as production data. The required --request-id is the caller's idempotency key, so retry the identical command after a lost response. Run wf backup <behavior-id> first when a recovery bundle is wanted; backup/export alone does not satisfy the terminal decision.

App release uses one exact authority document; the CLI never invents or refreshes parts of it during publication:

{
  "requestKey": "release-request-2026-07-12",
  "slug": "client-requests",
  "expectedFrontendRevision": 2,
  "expectedLiveDeploymentId": null,
  "expectedContract": "wfc_<43-character-hash>",
  "expectedDraftIdentity": "wfd_<43-character-hash>",
  "empiricalProofId": "empirical-proof-job-id",
  "fixtureDecisionReceipts": [],
  "dataMigrationPlanHash": "<43-character-hash>"
}

Retry only with the identical document. expectedLiveDeploymentId is the deployment observed during approval (or null when none was live), so a stale tab fails rather than replacing a newer release. expectedDraftIdentity pins the complete reviewed draft, including its Contract, Access policy, interface, and behaviors. fixtureDecisionReceipts must contain the canonical receipt ID and checksum for every accepted clear/promote decision; never default it to an empty array when decisions exist. dataMigrationPlanHash pins the live-record simulation reviewed immediately before approval; any intervening write makes release fail closed.

The check run ID comes from the dedicated whole-App browser check run. App build checks, kernel test scenarios, and endpoint scenario rehearsals may use older check wording in retained responses, but they are not empirical release checks and their IDs must never be passed to release.

For the chosen eligible check, review coverage.exercisedInteractions and every item in coverage.limitations. Hosted coverage v1 means the exact saved interface loaded. External coverage v2 is deliberately different: mode: "external-backend", backendContractLoaded: true, and externalFrontendExercised: false mean the deterministic internal Contract harness ran, not the App-owned product UI. Neither form is a claim of complete product coverage. coverage.limitations is a unique, canonical-order subset derived from capabilities that are present in that exact App but remain outside the executable check plan: authenticated or role-gated actions, manual-review continuations, Checkout, ineligible or partially covered entity mutations and transactions, file transfer, app job/cancel lifecycle, required or server-set stateful command inputs, anonymous visitor ownership continuity, production connections and side effects, or interactions beyond the first 32 canonical executable actions. An absent class is not reported as a generic boundary.

Eligible anonymous scalar entities add an entity-lifecycle interaction. That probe uses one isolated Browser session to verify create, point readback, CAS patch without dropping untouched fields, stale-write rejection, atomic transaction commit and rollback (including idempotency rollback), wrong-owner denial, delete, and final absence. It names only the exercised entity; partial-entity-mutation-coverage remains for replace or additional entity mutations. no-anonymous-ownership-lifecycle remains because this isolated probe does not exercise production visitor cookie/token issuance. Its generated values, record identities, ownership capability, cookies, and bodies never enter retained evidence.

The product-level MCP loop is inspect_app → describe kind:"flow" → edit text → apply {text, expectedRevision} → test → frontend draft journey → capability-preflight get_app_release_authority → empirical checks → current authority → publish. build_app and edit_app are optional AI shortcuts. When compilation or validation takes longer than 45s, build_app and edit_app answer with a pending status (status: "building" or status: "editing") and their requestKey or requestId before the client's tool deadline; retry with the same parameters and key to wait for completion or receive the saved result. A finished build_app, edit_app, or update_app_frontend reply is a short status, normally a few KB and always under 8 KB, so an agent client reads all of it. edit_app returns state (applied, unchanged, proposal, clarification, or failure), applied, the new revision, a bounded summary, changes, operationResult and error, frontend as {revision, frontendLane, htmlBytes}, any refusal in plain words, and a details block naming the exact call for each larger body: inspect_app with section:"frontend" for the interface HTML (it arrives in get_result_page pages when over 64 KB), describe kind:"flow" for the Flow text, or inspect_app for structure. Interface HTML and graphs are never inlined; a reply that would still be too big drops its optional parts in a fixed order and lists them in omitted. A sensitive proposal keeps the stateless signed proposal.token inline only while it is 2 KB or smaller; a larger token is replaced by tokenOmitted: true and tokenBytes, and the owner reviews and approves the whole proposal at proposal.approval.url while signed in (an API token cannot apply it), after which get_edit_proposal_status reports applied. update_app_frontend answers {saved: true, frontend: {revision, htmlBytes, ...}} and no longer echoes the HTML back; build_app keeps app, flow: {id, name}, frontend (compact), and proof under their existing names. Existing field names are unchanged; what changed is that frontend.html and flow.graph are no longer inlined in build_app and update_app_frontend replies (edit_app already left them out). Stop at the draft when authority, checksAvailable, an authorized publishing path, or present-operator review is unavailable. Present the full exposure block before checks and again before release; MCP release requires operatorReviewedExposure=true as an explicit acknowledgement. generate_app_client returns the checked TypeScript adapter for React, Angular, Vue, or another local project. inspect_app returns frontendLane, frontendRevision, and frontendUpdatedAt, so an external project can resume without fetching the saved handoff HTML. inspect_app with section:"frontend" and update_app_frontend remain HTML-only escape hatches with optimistic concurrency; a stale revision gets HTTP 409 instead of silently overwriting another tab.

export_view_csv (pipelineId, view, optional orderBy and testUserToken; SDK exportViewCsv(appId, { flowId, view, … })) downloads a named view of the draft as CSV: the rows its JSON read returns for the caller, the view's columns in order, spreadsheet formulas neutralised, 10,000 rows at most (a larger view is refused in plain words with 413, never cut short). describe lists the route under csvExports for each view and the App Contract lists it under exports; a released App serves the same POST /_wrapflow/flows/<flowId>/views/<view>/export.csv on its own origin.

Use a read + draft API token for an AI. It can inspect backend context and edit HTML, but publishing a public *.wrapflow.app deployment requires an admin token and explicit operator approval. Never put an account API token inside generated HTML.

Hosted v5 source contains only scriptless semantic HTML, inline CSS, and exact data-wrapflow-* Contract hooks. Wrapflow's trusted parent shell owns the client, transport, authentication, actions, and result projection; presentation bytes receive no account token, route-building authority, or raw response. JavaScript and framework projects belong in the external frontend lane: install the browser-safe wrapflow-app runtime through the generated adapter and use its named resources. The external client and hosted compiler both verify /_wrapflow/contract.json; OpenAPI 3.1 is at /_wrapflow/openapi.json. Transport paths, auth headers, version checks, and stable error parsing remain outside model-authored presentation.

As a backend for static pages (MCP)

A published App can expose selected released queries and commands as MCP tools. wf app mcp-access authorize <app-id> defaults to published queries only. Use --tools <query,command,command> to opt in to writes, or the retained --read <query> --write <command> pair. --expires <ISO-timestamp> optionally limits the grant's lifetime. The account MCP tool manage_access mcp-grant/authorize and SDK authorizeAppMcpAccess accept the same resources and expiresAt selection.

The result includes a token-free mcpUrl (https://wrapflow.com/mcp/apps/<app-id>) and a wfm_ token shown once. Header-capable remote MCP clients use that URL with Authorization: Bearer <token>; never put the token in the URL. The endpoint supports initialize, tools/list, and tools/call. Tool schemas pin the live compatibilityHash; writes require confirm: true and an idempotency key. The grant cannot build or publish, reach another App, or invoke an unselected operation. Revoke with wf app mcp-access revoke <app-id>.

For released scriptless hosted interfaces (manifest v5), the per-App HTTP MCP also supports resources/list and resources/read. Granted tools link to a release-specific ui:// resource through _meta.ui.resourceUri; the resource returns the published interface as text/html;profile=mcp-app for an MCP Apps host to render inline. The payload contains no injected account or grant token. Its CSP denies scripts and all network access, including images and fonts. This first slice is an interface preview: live actions still run through the granted tools. UI RPC interactions and live updates are not connected in this slice; rendering inside Claude.ai/ChatGPT remains unverified. External and executable frontends keep their tools without an interface resource. The public web App retains its embedding restrictions.

A direct HTTP client of POST https://wrapflow.com/mcp (Bearer token, JSON-RPC tools/list) must set its own User-Agent. Python 3 urllib.request sends Python-urllib/3.x when you leave the header unset. Cloudflare refuses that signature with HTTP 403 error 1010 before Wrapflow sees the token, so the body is Cloudflare's access-denied page rather than a Wrapflow error. Send a header that names the client, such as User-Agent: my-script/1.0. The same call then reaches Wrapflow. wf mcp over stdio does not make this HTTP call.

A grant follows the exact operation you chose, by its kind (query or command) and its identity in the App's Contract, never by name. A query and a command may share a Contract name; select them as <name>_query and <name>_atomic_command (a plain <name> selects the query). A later release can only remove tools from a grant, never add or widen one: if the release no longer has that exact operation, or it became the other kind, the tool disappears and inspect and tools/list report it in dropped ("dropped from this grant because the operation changed"). Authorize again to grant the new operation.

URL-only Claude/ChatGPT connector authorization is not provided by this bearer endpoint; it has no server OAuth flow. The original stdio bridge remains: point wf mcp at WRAPFLOW_APP_MCP_TOKEN to list the same selected tools.

wf mcp starts a zero-dependency MCP server over stdio, so a coding agent can drive Wrapflow as the backend for whatever it is building. For a complete App, use the typed App Contract lifecycle above. The lower-level tools below remain for background app capability URLs (CORS is on for token-gated public endpoints):

  • publish with pipelineId → background app /f/ view + OpenAPI discovery + latest.json bounded table URL (+ form submit URL). A ready metric-only view keeps OpenAPI and callable routes available while latest.json returns 409 public_view_no_table_result. publish with appId uses the hosted App release authority instead.
  • enable_data_intake → POST JSON rows in (events, sign-ups, telemetry). The live intake needs an admin token. With target: "draft" it prepares the draft instead: nothing is enabled and no token is minted
  • send_test_events → a draft token's way to exercise a Live stream app on its draft: validated sample events go to the draft only (never the live stream or the released app), read: true reads them back, replace: true starts over
  • enable_invoke_endpoint → run the app per request and get its result table back in the response (respondWithResult)
  • run_workflow → run a background app now (draft or live). For apps with a Request step (source.request), pass optional request object on target: "draft"; published live Request apps are invoked via their invoke-v1 entrypoint. For apps with a Live stream step, pass events (sample rows, or "draft" for the saved test events) on target: "draft"
  • configure_run_api_file_target → with explicitly supervised owner/admin authority, persist the exact released File or Sample step used by authenticated Run API raw or multipart bodies. Generated App clients do not expose this owner control-plane setting.
  • integration_guide → the build order + copy-paste snippets

Register in an agent (e.g. Claude Code .mcp.json):

{ "mcpServers": { "wrapflow": { "command": "node", "args": ["./node_modules/wrapflow/wf.mjs", "mcp"] } } }

Auth comes from wf login config, or WRAPFLOW_URL + WRAPFLOW_TOKEN env vars. A plain wfh_… endpoint uses the URL token as its capability. Published or invoke endpoints configured with BYO app-user auth additionally require WRAPFLOW_APP_USER_TOKEN; protected async job status/cancel calls must use the same app user that started the job.

Interface posture

The supported creation interfaces are a plain-English prompt (wf create "…" or wf app build, the same AI path as the app) and the deterministic kernel verbs (wf app create, wf create --empty, wf app attach, wf describe, wf test, wf define) — never hand-authored graph JSON. The behavior's graph schema is an internal, unstable detail: it is necessarily visible (the editor and the export feature ship it), but it is not a contract. The hidden --graph-json flag exists for our own tests and may change without notice.

Auth model

wf login exchanges email+password for a long-lived least-privilege API token (read+run+draft) stored in ~/.config/wrapflow/config.json. It can build and test drafts and edit App HTML, but can never release an app behavior, publish App HTML, rotate credentials, or touch live records — releasing and deployment stay yours. Human operators can deliberately run wf login --admin; the CLI prints a high-authority warning, and wf mcp refuses that token unless the single process explicitly sets WRAPFLOW_MCP_ALLOW_ADMIN=1. Environment-based MCP configuration must declare opaque-token metadata with WRAPFLOW_TOKEN_SCOPES=read,run,draft. Passwords and browser session cookies are never written there. wf stream uses the API token to explicitly rotate a fresh intake secret; ordinary entrypoint reads remain redacted. The config directory is mode 0700 and the secret-bearing config file is forced to mode 0600 on every write. A new login replaces all prior account state, including saved stream aliases. Use the hidden prompt or pipe a password with --password-stdin; --password is rejected so a password cannot be put in shell history or process arguments by mistake.

wf run defaults to --target draft (or --target live explicitly). After changing a graph, run the draft; wf release automatically uses the latest successful draft run (or pass --from-run <run-id>). Production refreshes name --target live explicitly. When publishing a public view, choose which columns to expose with --columns <table>=<col,col> (or configure explicit column projection on the table). Pass --confirm-broad-tables only as a last resort (this makes every column of public).

Ask the owner for a service key

Request a key without passing its value through the agent:

wf secret request --name "Webhook key" --purpose "Post approved events" --host api.example.com --auth bearer --flow <flowId>
wf secret status <requestId>
wf secret attach <requestId> --flow <flowId> --step <stepId>
wf secret revoke <requestId>

Show the returned Wrapflow URL to the person. Only the signed-in owner can enter an API key or token there, or choose a matching saved connection. The link expires in 30 minutes and accepts one successful submission. Status returns only connectionId. wf secret attach binds that connection to one step of the Flow (the step's connectionId) once the request is fulfilled; the agent passes ids only and never sees the key. The step must call the host the person allowed. Reading the Flow afterwards shows uses connection 'Webhook key' (set by the owner) on the step, never the value. Never ask for an account password or a key in chat. Authentication can also be apiKeyHeader with --header-name X-Service-Key, or basic with owner-entered username:API-token. The host is mandatory and cannot change after the request. --flow is optional. The owner can inspect and revoke requests in the Flow's See inside view.

The SDK methods are requestSecret, secretStatus, attachSecret, and revokeSecret. Both MCP transports expose manage_access secret/request, manage_access secret/status, manage_access secret/attach, and manage_access secret/revoke. These accept request metadata or a request ID; they never accept the value.

Deploy a built UI from GitHub Actions

Your CI builds the UI; Wrapflow hosts it. An App created with --frontend external takes your static build on its draft. The owner (signed in, or an admin token; not the read+run+draft login token) creates a deploy token for that one App:

wf app deploy-token create <app-id> [--name "GitHub Actions deploy"] [--expires-in-days 90]
wf app deploy-token list <app-id>               # names, prefixes, last use; never the secret
wf app deploy-token revoke <app-id> <token-id>  # its uploads then answer 401

The secret is printed once. Store it as the repository secret WRAPFLOW_TOKEN (and the base URL as the variable WRAPFLOW_URL). Then a job like this uploads each build to the draft (add wrapflow to the project with the pinned install shown at the top, and run the installed copy, not npx):

on: { push: { branches: [main] } }
jobs:
  deploy-ui:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 22 }
      - run: npm ci && npm run build   # your build; writes dist/
      - run: node ./node_modules/wrapflow/wf.mjs app upload-ui --app <app-id> --dir dist
        env: { WRAPFLOW_URL: "${{ vars.WRAPFLOW_URL }}", WRAPFLOW_TOKEN: "${{ secrets.WRAPFLOW_TOKEN }}" }
  • Draft only. The token uploads that App's UI to its draft and reads back the upload status, nothing else: it cannot release, read or change App data, reach other Apps, or read secrets. Releasing still needs the owner's approval, so a push never changes what people see on the live App.
  • Source link. Each upload records where the build came from, and the owner sees it when reviewing (Source: linked to github.com/acme/portal @ a1b2c3d4e5f6 in get_app_ui, bundle.source in wf app ui). In GitHub Actions the link comes from GITHUB_SERVER_URL, GITHUB_REPOSITORY and GITHUB_SHA; elsewhere from git remote get-url origin and git rev-parse HEAD when git is present. Pass --no-source to skip it. The API takes source: { repoUrl, commit }, an https address and a 7 to 40 character commit SHA.
  • Limits and expiry. An App has up to 5 live deploy tokens, separate from your own API tokens, which have no count limit. --expires-in-days sets an end date; an expired token answers 401 deploy_token_expired.

MCP exposes manage_access deploy-token/create, manage_access deploy-token/list and manage_access deploy-token/revoke; the SDK has createAppDeployToken, listAppDeployTokens and revokeAppDeployToken (REST: /api/apps/<app-id>/deploy-tokens).

Read a Space as one system

wf system [<space-id>]

Returns the Space's tables, Apps (with their own backend), backends whose UI is hosted elsewhere, agents and outside services, with one line per pair named after what moves data along it or "n connections", health notes and groups of things that work together. Defaults to the current Space; read-only. The SDK method is getSystem(spaceId?) and MCP exposes get_system. When you host an App's UI yourself, wf app integration ui-host <app-id> <https://your-ui…> (--clear to remove; SDK setAppUiHost, MCP set_app_ui_host) records where, so its Backend node reads "UI hosted by ".

Change a backend with the owner's approval

An agent proposes one additive change (entity, command or view, the same spec define takes; a scheduled pull is defined on the draft and released, never proposed). Wrapflow returns a plain-language summary and a one-hour owner link, and changes nothing until the signed-in owner approves.

wf change propose --flow <flowId> --kind entity --spec '{"name":"reservations","fields":[...,{"name":"phone","kind":"text"}]}'
wf change propose --flow <flowId> --changes '[{"kind":"entity","spec":{...}},{"kind":"command","spec":{...}}]'
wf change status <proposalId>
wf change history --flow <flowId>

Adding fields, entities, commands and views is accepted. Renames, removals, type changes and tightened rules are refused with the reason. Related changes, such as a field and the command that uses it, go in one proposal as changes (up to 5, applied in order, one approval); MCP and the SDK take changes: [{ kind, spec }, ...]. The SDK methods are proposeChange, changeStatus, revokeChange and changeHistory; MCP exposes propose_change, change_status, revoke_change and change_history. History includes direct saves from define and apply, as well as approved proposals. An entry with kind: "definition" carries revision (author, time and redacted diff) and createdAt; an approval carries proposalId and approvedAt. MCP Flow-target tools accept appId for an App with one Flow, or pipelineId for an explicit Flow. test {appId} defaults to all saved scenarios; test {appId, scenario: {spec: candidate}} runs those plus the candidate without saving it.

If the draft changes before approval, status returns stale with a message asking for a fresh proposal. Propose the change again against the current draft; waiting on the old link cannot apply it.

MCP access operations use the single manage_access tool with {kind, action, input}. Below, manage_access secret/request is shorthand for manage_access {kind:"secret", action:"request", input:{...}}; every operation's fields go inside input. Sign-in still needs owner approval; deploy tokens still need owner/admin authority; test users remain draft-only.

Turn on sign-in for per-person rows

Entities with rows or access need verified hosted sign-in, which only the owner can turn on:

wf signin propose --flow <flowId> --signup anyone|invite|allowlist [--allow [email protected],@x.com]
wf signin status <proposalId>
wf signin revoke <proposalId>

A proposal changes nothing. Show the returned URL to the owner; only they can approve it while signed in. The link expires in one hour. After status reads approved, wf advanced define <flowId> entity '{"name":"tasks","rows":"own",...}' succeeds. The SDK methods are proposeSignIn, signInStatus and revokeSignIn; MCP exposes manage_access signin/propose, manage_access signin/status and manage_access signin/revoke.

SDK proposeSignIn and MCP manage_access signin/propose accept exactly one of appId or pipelineId. MCP describe accepts the same targets. An App ID resolves only when the App has one Flow; with multiple Flows, pass a pipelineId explicitly. For a fresh create_app result, call define with appId first, then use the pipelineId returned by define. Describing or proposing sign-in creates no Flow.

Once sign-in is on, a table's access lists who may see a row: "creator", "signed-in", {"person":"<text field>"} (the field holds the person's App-user id), {"email":"<email field>"}, {"role":["<role>"]} and {"members":"<membership table>","via":"<reference field>"} for organisations and queues (the membership table declares "membership":{"person":…,"group":…}). A person sees a row when any one holds; the server applies it to queries, views, counts, live updates, references and files. wf advanced describe <flowId> entity shows each table's access and its whoSees sentence:

wf advanced define <flowId> entity '{"name":"orders","access":{"read":["creator",{"person":"seller"}]},"fields":[...]}'

Changing another person's row needs a patch or delete command that declares edits with the same relations plus "readers" (anyone who may see the row); without it a command is the row creator's, whatever its name:

wf advanced define <flowId> command '{"name":"reassign","entity":"tickets","operation":"patch","fields":[{"name":"assignee","kind":"text"}],"edits":["creator",{"role":["lead"]}]}'

Sharing and binding tables across Apps (Tier 5 shared inventory)

An App-owned table can be bound by another App by reference (table: "app-entity-v1:<appId>:<name>" or <appId>/<name>), without copying rows. When two distinct Apps (for example a warehouse admin app and a customer storefront) need to read or mutate the same table:

# In App B: bind App A's inventory table by reference without copying rows
wf advanced define <flow-or-app-id> entity '{"name":"inventory","table":"app-entity-v1:<app-a-id>:inventory","fields":[{"name":"sku","kind":"text"},{"name":"stock_level","kind":"number"}]}'

# Define local orders entity referencing inventory
wf advanced define <flow-or-app-id> entity '{"name":"orders","fields":[{"name":"product","kind":"reference","entity":"inventory","required":true},{"name":"qty","kind":"number","required":true,"min":1}]}'

# Define atomic take command that deducts stock from App A's table
wf advanced define <flow-or-app-id> command '{"name":"place-order","entity":"orders","guards":[{"source":"inventory","where":[{"field":"_wf_record_id","op":"eq","valueField":"product"}],"column":"stock_level","take":{"amountField":"qty"},"message":"Not enough stock for this order."}]}'

App B reads and queries App A's live rows directly, and place-order commands in App B atomically deduct from App A's inventory under the write coordinator. If the order write fails or is rejected, the taken stock is automatically given back.

Creating the account itself

wf login signs in; it does not register. There is no CLI verb for creating an account, and there is no POST /api/auth/signup route — an unmatched /api/* path falls through to the generic authentication guard, which answers 401 "Sign in to use Wrapflow." even though nothing about your credentials is wrong. The real route is POST /api/auth/register:

# Session mutations are CSRF-guarded: send the exact Origin this deployment is
# configured with. GET /api/health is unauthenticated and publishes it as
# `expectedOrigin`, so ask rather than guess — it differs from the address you
# dial behind a proxy, tunnel, port-forward, or custom domain.
curl -sS -c jar.txt -X POST "$WRAPFLOW_URL/api/auth/register" \
  -H "Origin: $WRAPFLOW_ORIGIN" -H 'Content-Type: application/json' \
  -d '{"email":"[email protected]","password":"…"}'

Registration returns an HttpOnly wrapflow_session cookie. For ordinary CLI use you do not need to handle it: run wf login with the same email and password and it performs the whole exchange (POST /api/auth/login, then POST /api/tokens for a read+run+draft token, then POST /api/auth/logout to drop the session). Keep the cookie only when driving the HTTP API directly, and send it on every session-authenticated call.

docs/PROGRAMMATIC-APP-AUTHORING.md in the Wrapflow repository is the reference for this whole surface: registration, the Origin/CSRF guard and its 403 "Refresh Wrapflow and try again.", token scopes, free-plan limits and quotas, and the exact HTTP routes behind every command in this README. Read it before hand-rolling requests against the API. Product-using agents building Apps through this CLI should follow this README and docs/BUILD-WITH-WRAPFLOW-AS-AN-AGENT.md; neither is permission to change Wrapflow source.

Schedules

Draft tokens use wf schedule <id> --target draft --cron "0 21 * * *" to declare a schedule. With --target draft, omit cron to read it, use --disable to clear it, or --test-fire to run it once against draft data. The SDK uses draftSchedule(id, {cron, timeZone}), draftSchedule(id) and draftSchedule(id, {clear:true}); test with run(id, {target:'draft', schedule:'draft'}). MCP schedule accepts target:"draft" and testFire:true. Draft schedules never tick unattended. Tests read their own App preview collections and draft stream events; emails, HTTP outputs and cross-App commands are held. A Flow without an App cannot read live collections in a token draft run. Saved production datasets cannot be read or queried by token draft runs.

wf schedule <id> --cron "0 2 * * *" [--tz UTC] sets a Flow's recurring schedule, wf schedule <id> reads it, and wf schedule <id> --disable clears it. The SDK uses schedule(id, { cron, timeZone, enabled }) and entrypoints(id); the MCP tool is schedule (pipelineId, then cron and optional timeZone to set, disable: true to clear, neither to read). All three use the existing entrypoints API and return the schedule as the server saved it. Enabling a schedule requires a released Flow; setting or clearing it requires admin permission. Reading and clearing a previously stored draft schedule do not require a release. After creating a background app (e.g. via wf create or the MCP create_workflow tool for a daily reminder or periodic job), verify and release the Flow, then call schedule with the returned id/pipelineId to configure its recurring schedule.