Project server API
touba serve connects an application to the Touba runtime over HTTP and SSE.
The application authenticates people and presents their cases; the runtime
retains authority over capabilities and effects.
Finish a session and archive state
| Action | Route |
|---|---|
| Request operation cancellation | POST /interactions/sessions/{session_id}/operations/{operation_id}/cancel |
| Archive a completed session | POST /interactions/sessions/{session_id}/archive |
Both calls accept an empty JSON body. Cancellation is cooperative: read the operation until it reaches a terminal state. Session archival retains its decisions and evidence.
To start with fresh server state during development, stop the server, resolve pending approvals and drain trigger queues, then run:
touba serve --check-state
touba serve --archive-state
touba serve --archive-state --apply
The preview lists files and sizes. --apply moves state into
.touba/state-archives/<id>/, with an inventory and file hashes.
Sources, packages, SDKs, manifests and secrets stay in place. This starts a
fresh effect journal: use it with controlled development data. Keep existing
state when continuing business operations.
Restoration refuses to overwrite current state:
touba serve --restore-state <id>
touba serve --restore-state <id> --apply
If archival is interrupted, the server waits for restoration. Use the build matching the restored state. Custom state paths require their own operational backup procedure.
Decision identifiers and comment
client_ref identifies the submitting application. evidence_ref identifies
the evidence read by the attestation adapter. Both may appear in a decision.
Approval.comment exposes an Optional of Text, limited to 4,096 characters.
It is user-entered text; subject_ref and attested_roles come from the
validated attestation.
Prepare and start
From the project root:
touba prepare --profile dev --mode write
touba prepare --profile dev --mode check
touba serve --project-root . --host 127.0.0.1 --port 8791 \
--auth-token-env TOUBA_SERVER_TOKEN \
--identity project-identity.json \
--agent-decision-provider providers/decision.mjs
Set a strong TOUBA_SERVER_TOKEN in the host environment. Never commit it or
send it to a browser. Omit --identity when your --human-approval-host adapter
attests approvals from the application directory, or when there are no human
decisions. Omit the provider when no agent decision is needed.
A provider module is trusted host code, not client-supplied code.
The server consumes .touba/build/dev/ directly and verifies input digests
before execution. No AIR copy is needed. After editing, run prepare again,
then POST /project/refresh. Stale preparation is refused. This development
sandbox is not production activation. Without a preparation, explicitly
compiled project AIR files remain supported.
Provider options also include --agent-decision-provider-metadata-json,
--agent-decision-provider-content-mode inline|opaque_refs,
--agent-decision-provider-initial-mode summary|capabilities,
--agent-decision-provider-timeout-ms and
--agent-decision-provider-max-retries. See the SDK guide.
Tool secrets come from touba.secrets.dev.json or an explicit --secrets profile.
If you maintain an explicit manifest (including trigger activation without tools), pass --manifest touba.manifest.json to both prepare --mode write and prepare --mode check. Otherwise preparation uses its derived manifest, not an implicitly selected root file.
The --identity file must stay inside --project-root; relative paths resolve from that root. Outside paths fail with ToubaServeProductIdentityPathInvalid, detailed under ToubaServePreflightFailed. Keep this host configuration inside the project without committing secrets or personal identifiers.
Project entrypoints and hosts
Declare the workflows the application may start in touba.project.json:
{
"entry": "strategy:app.expenses/ReviewExpense",
"entrypoints": ["strategy:app.expenses/Reconcile"]
}
entry selects the default workflow; entrypoints adds other workflows.
After a change, run touba prepare --mode write. Select a workflow with
touba run --project . --entry strategy:app.expenses/Reconcile.
The server publishes these entries in GET /project and checks the same list
before execution and resume. Internal agents are called by their authorized
strategies. Keep internal write tools off this list when a strategy owns
their approval procedure.
Both --agent-decision-provider and --human-approval-host accept JavaScript
(.mjs) or Python (.py) modules. Install the Python SDK with
touba sdk install python; the server uses the .venv interpreter.
A module selected for both roles is loaded once. Its Python process closes
when the server stops.
| Exported role | JavaScript | Python |
|---|---|---|
| Agent decision | decideAgentCapability | decide_agent_capability |
| Human attestation | attestHumanApproval | attest_human_approval |
| Fixed initiator | initiatorRef | initiator_ref |
| Attested per-run initiator | resolveInitiator | resolve_initiator |
| Initial mode by agent | initialModes | initial_modes |
The initiator resolver receives run_id, entry_ref and inputs. It verifies
application evidence before returning the initiator's opaque identifier.
Resume preserves that identity. The client-supplied requested_by field
is descriptive, not identity evidence.
With an approval host, the application owns its directory and authenticates
its users. The server passes { request, decision, evidenceRef, hostDecision }
to the adapter. The POST may include an opaque evidence_ref; otherwise
evidenceRef uses request_id. The adapter verifies the request, signer and
current rights. The attested signer must match user_ref and decided_by.
The public request exposes policy: role, timeout and signer separation.
Accepted decisions retain their receipt. Removing the adapter after restart
does not remove the attestation requirement. The --identity workflow remains
available without an adapter; agent-initiated reviews retain this check.
Execution routes
Application requests carry Authorization: Bearer <token>; JSON POSTs carry
Content-Type: application/json. Contract names below are exact
schema_version values, not software distribution versions.
| Method and route | Request contract or purpose |
|---|---|
GET /health | process availability |
GET /project | inspection, entrypoints and executable signatures |
POST /project/refresh | touba-project-refresh-request-v1 |
GET /identity | project users, devices and assignments |
POST /runs/sandbox | touba-sandbox-run-request-v1, execution_kind: "entrypoint" |
POST /triggers/fire | same contract, execution_kind: "trigger" |
GET /interactions/sessions | bounded session index |
POST /interactions/sessions | touba-interaction-session-create-v1 |
GET /interactions/sessions/<session_id> | messages, results and operations |
POST /interactions/sessions/<session_id>/messages | touba-interaction-message-request-v1; returns 202 with an operation |
GET /interactions/sessions/<session_id>/operations/<operation_id> | operation status |
GET /approvals/pending/current?run_id=<run_id> | touba-approval-request-v1 |
GET /reviews/pending/current?run_id=<run_id> | touba-human-review-request-v1 |
POST /decisions | touba-approval-decision-v1 or touba-human-review-decision-v1 |
POST /runs/sandbox/resume | { "request_id": "..." } |
POST /interactions/sessions/<session_id>/resume | touba-interaction-resume-request-v1 with request_id |
GET /events | SSE; Last-Event-ID resumes within available retention |
The two execution routes share a schema but require different discriminants.
Sending a trigger to /runs/sandbox is refused. For a trigger, supply the
event's event_id, source_kind and inputs (normalized target arguments) as required by the request
contract and declared trigger.
runtime_only refuses external effects. Explicitly choose
allow_declared_external_effects for workflows that require them. This
setting creates no grant and bypasses no approval.
Session, approval, decision, resume
- Create a session with
target_ref,target_kind: "strategy",presentation: "workflow",effect_policyandrequested_by. - Submit a message with a unique
message_id,display_text,inputsandrequested_by. Retainsession_id,operation_idandrun_id. - Follow the operation with SSE or its GET route. At
awaiting_human, fetch the request using that operation'srun_id, not the global latest run. - Present
summary,allowed_decisionsandexpires_at. Copy the exact correlations from the request into the decision:
{
"schema_version": "touba-approval-decision-v1",
"request_id": "request-id-returned-by-server",
"run_id": "run-id-returned-by-server",
"trace_ref": "trace-reference-returned-by-server",
"user_ref": "user:reviewer",
"device_ref": "device:backend-reviewer",
"decision": "approve",
"decided_by": "user:reviewer",
"decided_at": "2026-10-15T12:00:00.000Z",
"comment": "Receipt and supplier verified.",
"client_ref": "expense-backend"
}
- After decision acceptance (202), resume the session with its
request_id. Acceptance is not execution of an effect. Read the terminal result, including after rejection or expiry.
The summary contains only the explicit human.approve context, evaluated
at the pause and limited to 4,096 characters. Select a useful, minimal dossier;
truncated text is not a complete dossier. Common sensitive record fields are
redacted and resolved secret values are forbidden in checkpoints. This is not
universal personal-data detection: never place secrets in free text. Earlier
show messages and unrelated inputs are not copied. Private context stays
out of traces and SSE events.
Configure decision identities
--identity loads a host-managed touba-product-identity-v1 file. This is
neither a signup endpoint nor authentication. A minimal local configuration
for a trusted backend is:
{
"schema_version": "touba-product-identity-v1",
"generated_at": "2026-10-15T12:00:00.000Z",
"summary": {
"user_count": 1, "team_count": 0, "device_count": 1,
"active_assignment_count": 0, "revoked_device_count": 0
},
"users": [{
"user_ref": "user:reviewer", "display_name": "Expense reviewer",
"email": null, "status": "active", "team_refs": [],
"role_refs": ["role:reviewer"]
}],
"teams": [],
"roles": [{
"role_ref": "role:reviewer", "name": "Reviewer", "scope": "human_decision"
}],
"devices": [{
"device_ref": "device:backend-reviewer", "user_ref": "user:reviewer",
"kind": "service", "status": "paired",
"paired_at": "2026-10-15T12:00:00.000Z", "revoked_at": null
}],
"sessions": [], "assignments": [], "escalation_policies": [], "events": []
}
The user must be active and the device paired to that user. The
human_decision role is project-wide. To restrict a reviewer to a case, use
an active assignment correlated with request_id, run_id, trace_ref,
user_ref and device_ref, without also giving that user the global role.
Do not bypass checks to work around an expired assignment or revoked device.
Assignment authority requires an exact request/run/trace, user and device match,
an active status, and assigned_at <= server time < expires_at when accepting
the decision. Equality with the deadline rejects the assignment. A global role
remains separate authority; remove it when assignment-only access is intended.
To require distinct signers, declare distinct_from as "previous" on the
approval and configure an attestation host. "initiator" excludes the run's
attested initiator; exclude_signers names a cross-run exclusion rule
verified by that host.
The shared Bearer token authenticates the backend, not each person.
For multi-user clients, configure --identity-control-plane-url and
--identity-service-token-env, then forward the human token in
x-touba-identity-token. The server binds the actor to the introspected user.
Request reads require decisions.read; decisions require decisions.act,
in addition to device and assignment checks. Remote identity services require
HTTPS. The application remains responsible for real authentication and
business separation of duties.
Authority graph and available evidence
GET /authority/graph identifies each compiled approval site separately. Two approvals in one strategy remain two nodes; source scanning does not duplicate them.
GET /runs/current returns a summary and GET /trace/current a minimized timeline. For native evidence, use GET /runs/<run_id>/trace: each initial execution and continuation retains its own segment, original events and AIR commitment, without renumbering.
The touba-run-trace-export-v1 export contains segments and complete. Each segment carries its state (recording, paused, terminal, unavailable), digest and predecessor digest. complete: true means the initial execution and all segments are available and the run has terminated, not that its business task succeeded. After a process crash, a segment may remain recording with trace: null and reason: "not_completed"; missing evidence is never reconstructed. Historical runs without native evidence return 404.
The project token is required when configured, plus executions.read when identity control is enabled. No AIR, source, manifest, business message or private checkpoint is automatically discovered or included. For local audit, extract each available segment's trace and explicitly supply the AIR matching its run.air_digest. Local digests are not signatures.
Traces use .touba/traces.sqlite (512 MiB of serialized content by default),
with a separate metadata index limited to 128 MiB of SQLite pages and 256 segments
per run. The index quota counts bytes, including metadata, without an arbitrary
run-count ceiling. The server streams complete exports. Its preview retains the last
200 events and the full count.
Use GET /runs/<run_id>/trace?index=true for the touba-native-trace-index-v1
segment index, then /runs/<run_id>/trace/segments/0?limit=100 for a page.
Pass the returned next as after until done: true. The same project
permissions apply to both routes; mobile-only tokens cannot access them.
Archive the complete project state with the server stopped to preserve the links between traces, checkpoints and effects. A full journal stops progress before the next effect. No automatic deletion is enabled.
Startup and offline state checks validate retained evidence in batches of 100 records, capped at 16 MiB. Every batch is checked; historical corruption prevents recovery. This audit takes longer as history grows, while a current decision selects its records through indexed identifiers. Interaction recovery selects operations in the interface window and reads their checkpoints in pages. Older evidence remains stored and checked by the audit. These internal scans follow physical storage order; API cursors keep their identifier order. The validation cache keeps at most 16,384 SHA-256 fingerprints per validator, without retaining documents. Each read parses the current JSON text; a known fingerprint allows reuse of its pure validation. Authority checks still run. Each write validates the object before serialization and checks that the records read have not changed.
Operations and concurrency
Sandbox results and session messages carry show output in messages,
separately from the result returned by give. A client can display these messages
in order, including before a human pause. A continuation adds only its new
messages; retrying the same resume request does not duplicate recorded output.
This projection retains at most 100 messages, 4 KiB of text per message and
16 KiB total, without splitting UTF-8 characters. messages_truncated signals
any shortening or omission. Content remains private business data exposed
through authorized result/session routes, never the minimized timeline or
progress SSE notifications. It is not a native audit trace.
The V1 development contract requires these fields in new results. An older incomplete human-wait result is refused without rewriting its file: retain the data and qualify migration before replacing a server with pending waits.
Keep correlations per operation, use SSE, and recover the existing operation after a disconnect before starting a fresh run. Interaction history is bounded to 100 sessions, 100 messages and 50 operations per session. Durable waits and decisions have a separate lifecycle; do not delete their store to free history.
Checkpoints, human requests and decisions, effect journals, sessions and asset
metadata in touba serve use private SQLite stores under .touba:
sandbox-checkpoints.sqlite, human-requests.sqlite, human-decisions.sqlite,
project.effect-journal.sqlite, interaction-sessions.sqlite and
interaction-assets.sqlite. A targeted operation reads and validates its
records, then writes them in a short transaction with revision checks.
Permissions and deadlines are never cached. Structural validation of strictly
identical content may be reused in a bounded cache, without retaining an
invocation's authorization.
Startup retains an exhaustive audit whose cost grows with history. The decision-history API still returns the complete history; it is not paginated. The 100-session interaction window remains a bounded projection: replacing inactive entries does not delete checkpoints, decisions or effects. Durable writes are synchronized to disk; measure their cost with your handlers and volumes. This local storage does not qualify multiple servers sharing a disk and does not promise fixed latency.
Synchronous progress events are batched before persistence and SSE notification. The counter retains every event and the window retains the latest 80 events. A crash may lose the final progress batch: this UI projection is not evidence of an effect. Checkpoints and the effect journal retain separate persistence.
There is no implicit one-run-per-second quota. Measure HTTP admission, handler execution, state reads and client refresh delay separately. Handlers and storage can limit throughput. Combine the Touba effect journal with service-side idempotency keys and reconciliation when needed: this is not a universal exactly-once guarantee.
Operations, backups and build changes
touba serve --check-state also checks links between human checkpoints and
interaction sessions, using private copies. An orphan link not allowed by the
retention rules is refused, just as at startup. Preflight never repairs or
modifies the original files.
Keep touba version with the exact CLI archive, SDK and configurations.
Two development builds labeled 1.0.0 are not separate public releases;
compare their build identities and documented changes before replacement.
For a coherent backup:
- Stop new admissions and trigger producers, wait for active handlers to finish, then stop the server. A persisted human wait can be backed up; do not delete it.
- Back up sources, handlers, dependencies, manifest, configurations, locks
and the complete
.toubadirectory together. Include explicitly configured state paths outside that directory. Effect journals, checkpoints, identities, decisions, sessions and native traces form a correlated set. Protect and encrypt copies containing secrets or business data, preserving access rights. - Restore in isolation first, using the same toolchain and the same paths for absolute configuration references. Check waits, deadlines, native evidence and effect journals before enabling effects. A backup does not pause time.
The qualified restoration preserves a wait's identifier and native evidence, resumes after a decision and does not redispatch its effect on a second resume. This assumes a coherent snapshot and no unreconciled external effect since that snapshot. Restoring local state cannot restore an external database, payment or email: reconcile external records before resuming.
Development-state compatibility
The current server refuses previous JSON stores, including empty stores. If an
old default JSON store remains beside its configured SQLite store, startup is
refused before recovery or execution. Keep a coherent stopped copy with its
original build; do not rename JSON to SQLite or erase history to force startup.
The separate touba run --human-decisions JSON input is unchanged.
Persisted results now require messages and messages_truncated. Incomplete
historical results are refused without rewriting them. Do not invent an empty
history or delete the store to start the server. Test a stopped copy of state
with the new installation before upgrading. If incompatible, retain the old
installation in isolation and finish its waits with its own state and valid
decisions.
After reconciling operations and effects, retain that environment as a protected archive. Prepare a new instance from explicitly reviewed sources and host decisions, without importing incompatible state. Historical identifiers, decisions and evidence remain attached to the archive; they are not merged or presented as native history of the new instance. No automatic historical-state converter is provided. Unresolved waits block this transfer.
Operational boundaries
| Need | Engine/CLI | Project server |
|---|---|---|
| Human wait | explicit host-managed checkpoint/resume | persisted wait and resume correlated with an authorized decision |
| Deadline | Engine does not provide an automatic expiry timer | fixed 24 hours after creation, enforced against server time |
| Decision identity | host application's responsibility | host attestation, or local identity/device and role/assignment checks |
| Native trace | run.trace or --trace-out | GET /runs/<run_id>/trace segments with explicit completeness |
OpenTelemetry export is a projection, not a replacement for native audit evidence. File export is available; native collector delivery is not provided. See trace audit and human-wait expiry.
See SDK, triggers, idempotent effects and agents and outbound effects.
Before changing builds, use the state preflight and preserved build.
Approval capacity
touba serve --project-root . --max-pending-approvals 1000
The default is 100 and the maximum is 10,000. Each wait reserves capacity before pausing. At capacity, a new wait is refused before its continuation. Lowering the limit after restart preserves existing waits and still allows them to be processed. Choose the limit using measurements of your host and workload.
Trace events carry observed_at, the UTC instant observed by the runtime.
sequence_index remains the ordering reference, including when the system
clock changes.