What a request for "call module X from our app" brings along: identity, the tenant, the job envelope, reading back, learning about changes, and what to keep on the app's side. Written for the console, walmart-mvp, speedway, and App V5 alike, since the contract's promise is that they all do it the same way.
Audience: an agent or a person wiring an app to a module, on any cloud.
What the app brings along#
- A client credential per environment at the issuer, with the tenants
it may act for and the scopes it needs. The app exchanges it for a
short-lived token and never forwards a user's session or JWT
(
../canon/02-identity-and-tenancy.md). Until the issuer exists, a per-module key with the same request shape. - A tenant on every request, from the app's own org or workspace model.
walmart's
X-Org-Idvalidated against membership, speedway's workspace scoping, are exactly the source; the app puts it on the request asX-Tenant-Id. - A callback receiver if it does not want to poll: one route, verifies
the HMAC signature, dedups on the event id, and is idempotent
(
../contracts/module-surface.md, "Callback events"). Plus a reconciliation sweep that lists open jobs from the module by state, so a lost callback is a delay. - A read of the manifest at integration time (and cached at runtime),
to validate what it sends, generate the capability's settings UI from
params, and build each item'sinputagainstitem.
Submitting#
Build the job envelope from the app's own records: client_job_id set to
the app's id for this work (a workflow run and stage, a Firestore run doc
id, a Postgres job id) so the create is idempotent and the app can join
outcomes back without a lookup table; item_id set to the app's item ids
for the same reason; attribution carrying the user and the workflow run;
callback_url and callback_events in settings if the app has a receiver.
The app's store stays the truth for its UI; the module's store is the truth
for module-side outcomes; the ids are the join
(../canon/03-jobs-and-state.md, "Caller-owned, module-mirrored").
Submit is O(1) and returns accepted or expanding; the app shows that
state rather than running.
Reading back#
- Status and counts for the progress UI, polled or pushed.
- Outcomes paginated by type: results to write into the app's records,
errors to show,
needs_reviewitems to put in the app's queue with the reason and evidence,skippedto note. - The per-item log route for the detail page a person opens when one item looks wrong; no shell, no bucket.
- Stats for the job page: throughput, ETA, error breakdown, cost.
- Usage events (or the stats rollup) for credits.
The app's own side#
- The customer's bar over
confidenceandjudged_by; holding an item the module called done is the app's call (../canon/10-human-in-the-loop.md). - The review queue, resolution, waive, and the teaching loop.
- Re-submitting resolved items as a new job with
parent_job_id. - Plans, credits, and enforcement before submit; the module only enforces hard ceilings.
- Per-tenant configuration passed as
paramsand item fields, never stored in the module. - Which variant to ask for per tenant (
settings), when the manifest offers a choice.
Local development of the app#
The module runs in-process or as a local process with zero env
(../canon/13-local-dev-and-debugging.md); the app points at
localhost with the local verifier's static token, and nothing in the
app's integration changes between local and prod except the base URL and
the credential.
Do-nots#
- Do not forward a user's credential to a module. Exchange the app's own.
- Do not submit without a tenant, or with a tenant the app has not verified the user belongs to.
- Do not invent a mapping table between app ids and module ids; pass the
app's ids as
client_job_idanditem_id. - Do not show
runningfor anacceptedjob. - Do not treat a callback as the only signal; sweep.
- Do not put the customer's bar or per-tenant config in the module.