tide serve
Run the HTTP control surface over the workflow runner.
tide serve runs a long-lived runner, a first-class human-task operations
console, and an HTTP /v1 control surface. Instead of invoking tide run per
workflow, you enqueue invocations over HTTP, inspect and resolve human waits in
the browser, and observe the invocation resume. With the default persistent
store, suspended invocations survive a restart.
Start the server
tide serve --workflows workflows --port 4100| Flag | Default | What it does |
|---|---|---|
--port <port> | 4100 | TCP port to bind on 127.0.0.1. |
--workflows <dir> | workflows | Workflow source root. A request for workflow <name> resolves <dir>/<name>/main.ts. |
--registry <url> | TIDE_REGISTRY_URL | Registry source for exact name@version references. Unversioned names remain local. |
--store <memory|fs|sqlite|postgres> | fs | Workflow journal store. Keep a persistent backend for restart recovery. |
--workers <n> | 1 | Worker-pool size. >1 runs invocations in parallel (one V8 isolate per OS thread). |
--argon <url> | — | Serve state.* against a remote ox runtime serve /v1 endpoint instead of the embedded Argon backend. |
The shipped binary serves with the embedded Argon backend by default; --argon <url> switches to a remote runtime. The server runs until the process is
terminated.
Registry-free production control service
tide-control-service is a locked-down remote-Argon binary for one exact
workflow package selected by deployment configuration. It admits only the
NAME@VERSION passed with --allowed-workflow; unversioned, other-name, and
other-version requests refuse before queue admission. It does not accept a
registry URL or shared registry token.
Build the immutable local package with the same deterministic package builder
used by tide publish:
tide package \
--name example-workflow --version 1.0.0 --entry main.ts \
--output /release/example-workflow-1.0.0.tar --json \
/release/example-workflow-projectStart the service with explicit CLI flags or their environment fallbacks:
tide-control-service \
--token-file /run/secrets/tide-control-token \
--bind 127.0.0.1:7790 \
--state-dir /var/lib/tide-control \
--local-package /release/example-workflow-1.0.0.tar \
--local-package-digest sha256:<64-lowercase-hex> \
--local-package-size <bytes> \
--allowed-workflow example-workflow@1.0.0 \
--argon-origin http://127.0.0.1:7780 \
--store fs --workers 1| Flag | Environment | Required |
|---|---|---|
--token-file | TIDE_SERVER_TOKEN_FILE | yes |
--bind | TIDE_SERVER_BIND | yes; loopback only |
--state-dir | TIDE_SERVER_STATE_DIR | yes |
--local-package | TIDE_LOCAL_PACKAGE | yes |
--local-package-digest | TIDE_LOCAL_PACKAGE_DIGEST | yes |
--local-package-size | TIDE_LOCAL_PACKAGE_SIZE | yes |
--allowed-workflow | TIDE_ALLOWED_WORKFLOW | yes; exact NAME@VERSION matching the package manifest |
--argon-origin | TIDE_ARGON_ORIGIN | yes |
--store | TIDE_SERVER_STORE | no; default fs |
--workers | TIDE_SERVER_WORKERS | no; default 1 |
The state root owns queue/, store/, and package-cache/. The service
rejects process-local memory storage. The token file must be a regular
non-symlink file with no group/other permissions. The package must be a regular
non-symlink file, not group/world writable, and must match the configured size,
digest, manifest name, and manifest version within the shared 128 MiB bounds.
Each workflow resolution revalidates every extracted entrypoint and import
against the retained verified package bytes, repairing the private cache or
refusing before execution. The filesystem-store startup gate rejects links or
special files anywhere below store/ and completes a write, file and directory
fsync, exact read, removal, and removal fsync probe below store/invocations/.
The filesystem queue applies the same recursive custody checks, then uses an
isolated private directory to admit, reopen, and verify one exact pending record
before durably removing the probe; the real queue is opened only afterward.
TIDE_SERVER_TOKEN is rejected so the token does not enter the plaintext
environment.
Startup binds no socket until the durable queue/store open, the package is
verified and materialized, the token is valid, and the locked remote Argon
schema/module/tenant identity passes read-only /v1 checks. It does not create
an Argon fork. GET /health then returns { "status": "ready" }; GET /ready includes non-secret package, storage, and Argon identity evidence.
Open http://127.0.0.1:4100 for the embedded Dispatch Ledger. It lists real pending tasks from the durable queue and journals—there is no separate frontend process or sample data. Select a task to inspect its workflow-authored context, choose a configured action (or enter one when the workflow leaves actions open), edit the JSON decision payload, add a comment, confirm, and follow the invocation through resuming to completion, failure, or its next wait.
The console checks every eight seconds while its tab is visible, supports manual refresh and J/K queue navigation, and keeps in-progress form input intact.
Workers
--workers sizes the pool of OS threads, each owning one V8 isolate, all
draining the shared queue. --workers 1 (the default) runs one invocation's
event loop at a time; --workers 4 runs up to four invocations in parallel.
Every worker shares one backend handle, so they dispatch state.* against the
same durable world.
Workers are internal to the server process and start by default; a separate worker command is not required. Tide does not currently expose external queue claims, leases, or heartbeats, so this is an in-process scaling model rather than a distributed worker protocol.
The /v1 surface
Errors use the envelope
{ error: { code, message }, requestId }, mirroring Argon's runtime serve.
Enqueue an invocation
POST /v1/workflows/{workflow}/invocations
POST /v1/workflows/{name}/versions/{version}/invocationsBody: { "invocationId"?: <id>, "input"?: <json> }. Supply a stable
invocationId for idempotent admission. It accepts 1–128 ASCII letters, digits,
-, or _. Input is canonicalized before admission. An unknown workflow name
is a clean 404. A new run returns 202 Accepted:
{ "invocationId": "…", "status": "pending" }Repeating an exact invocation id + pinned workflow identity + canonical input
returns the existing lifecycle with 200 OK and does not requeue. Reusing the
id with different workflow coordinates or input returns 409 invocation_conflict.
curl -s -X POST http://127.0.0.1:4100/v1/workflows/onboard/invocations \
-H 'content-type: application/json' \
-d '{"input":{"company":"Acme","name":"Ada","salary":120000}}'Check status
GET /v1/invocations/{id}?logCursor=0&logLimit=100{
"invocationId": "…",
"status": "pending | running | settlement_pending | settlement_running | settlement_blocked | completed | failed | rejected | legacy_unproven | suspended",
"error": "…", // present on failed
"pending": { "waitOrdinal": 0, "taskId": "…" }, // present on suspended
"workflow": { "name": "onboard", "version": "1.2.0", "digest": "sha256:…" },
"logs": [{ "sequence": 0, "level": "log", "message": "…" }],
"nextLogCursor": 1,
"logsRemaining": false,
"result": { "…": "…" }, // completed only
"resultDigest": "sha256:…", // completed typed result only
"settlement": "promote_on_success | discard_on_success",
"settlementBinding": {
"invocationId": "…",
"workflowRef": "rebalance@1.2.0",
"workflowName": "rebalance",
"workflowVersion": "1.2.0",
"workflowDigest": "sha256:…",
"projectDigest": "sha256:…",
"backend": { "backend": "argon", "target": "https://…", "tenant": "test0", "fork": "tide-run-…", "schemaIdentity": "sha256:…", "moduleIdentity": "sha256:…" }
},
"settlementIntent": { "binding": { "…": "same exact binding" }, "disposition": "promote | discard", "runStatus": "completed | failed" },
"settlementReceipt": {
"backend": "argon",
"desiredDisposition": "promote",
"terminalStatus": "promoted",
"settledAt": "…",
"tenant": "test0",
"fork": "tide-run-…",
"parentForkId": "main",
"forkPointTx": "…"
},
"settlementError": "…",
"settlementAttempts": 1,
"updatedAt": "…"
}A suspended invocation is parked on a hitl.wait; the pending object names
the wait to resolve. Pass the previous nextLogCursor on the next poll to avoid
duplicate console output. logLimit must be 1–500; logsRemaining tells a
client to continue from nextLogCursor. Exact-version routes persist their
resolved digest before enqueue. Local workflows receive a digest of their exact
source tree. Restart and HITL resume refuse identity drift. A validated result
and digest are durably retained with completed queue state and survive restart.
Non-completed statuses omit them.
The exact invocation, workflow, project, backend kind/target, tenant, fork, and
runtime schema/module identities are persisted atomically before backend
preparation. Target discovery may open the exact runtime/world and validate its
identity, but it cannot create the invocation fork. Preparation receives the
persisted target and may create or attach the fork only through that same opened
target lease; crossed target/world identities refuse. Embedded Argon records the
artifact's stamped schema hash and the opened Connection module hash, matching
the remote runtime identity surfaces. Terminal workflow output and its settlement
intent reuse that binding and are persisted before settlement. settlement_pending
is unclaimed settlement work;
settlement_running is claimed settlement work. Restart requeues either phase
without rerunning the workflow body. completed or terminal workflow failed
is published only after the matching receipt is durable. The receipt records
fork lifecycle coordinates; it does not claim a recoverable promote-event set.
Target drift, a missing remote Argon fork, or an opposite terminal state refuses
settlement. Settlement failures durably requeue up to three attempts, then enter
settlement_blocked; no workflow body is rerun. legacy_unproven marks records
written before exact binding/receipts whose backend terminal state cannot be
proved.
rejected is reserved for source or project refusal before backend preparation;
it therefore has no backend state or settlement receipt.
Tide persists the exact binding and a backend-preparation marker before calling the backend. After a
crash, marked work remains nonterminal if its source or project cannot be
recovered; it cannot be reclassified as rejected while a fork may exist.
Marker-less records written by older Tide versions are treated conservatively
as possibly backend-started.
After process death during preparation, a deterministic fork already observed
open at the exact bound target is reattached. An absent, contradictory, or
otherwise unprovable fork outcome refuses before workflow effects.
List invocations
GET /v1/invocations?limit=25&cursor=…limit must be 1–100. The response is stable by creation time and invocation
id and returns an opaque typed nextCursor when another page exists:
{
"items": [{
"invocationId": "signal_run_123",
"status": "completed",
"workflow": { "name": "rebalance", "version": "1.2.0", "digest": "sha256:…" },
"createdAt": "…",
"updatedAt": "…",
"resultDigest": "sha256:…",
"settlement": "promote_on_success",
"settlementReceipt": { "backend": "argon", "desiredDisposition": "promote", "terminalStatus": "promoted", "settledAt": "…" }
}],
"nextCursor": "…"
}The list never returns invocation input, logs, or the structured result. Fetch
one completed invocation for its bounded result. Like every /v1 route, the
list requires the configured bearer token.
Resolve a human wait
POST /v1/invocations/{id}/resolveBody: { "action": "approved", "payload": {…}, "comment": "…" } (action
required; payload and comment optional). This records the decision into the
journal and re-drives the invocation under the same id — the HTTP analogue of
tide resume.
curl -s -X POST http://127.0.0.1:4100/v1/invocations/$ID/resolve \
-H 'content-type: application/json' \
-d '{"action":"approved","comment":"verified"}'This invocation-level route remains available for compatibility. New operator clients should use the task-specific routes:
GET /v1/hitl/tasks
GET /v1/hitl/tasks/{taskId}
POST /v1/hitl/tasks/{taskId}/resolveThe list returns { "count", "tasks" } in stable creation/invocation order.
Each task includes its task and invocation IDs, workflow, wait ordinal,
invocation creation time (when recorded), status, and the safe
workflow-authored hitl.wait request. It does not return invocation input or
unrelated journal entries.
Task-specific resolution requires the current ordinal as a precondition:
{
"waitOrdinal": 0,
"action": "approve",
"payload": { "verified": true },
"comment": "Evidence checked"
}Configured actions are enforced. Empty actions, malformed JSON, and actions
outside that set produce structured 400 errors. A stale ordinal or a repeated
resolution produces a structured 409 (stale_ordinal or
already_resolved). Resolution is serialized within one server process across
the exact journal check, completion append, and requeue. The current
DurableStore contract has no compare-and-append primitive, so running multiple
serve processes against the same store is not an atomic multi-instance
deployment model.
Durability across restarts
With the filesystem queue and a persistent journal store, a suspended invocation
survives a tide serve restart: bring the server back up, POST …/resolve, and
it resumes from its journal. The runtime replays recorded operations rather than
reissuing them; effect-intent reservation does not remove the crash window
between an external host call and journal persistence.
Network security
Set TIDE_SERVER_TOKEN to require Authorization: Bearer … on every /v1
control route. Remote run, follow, and resume read that variable
automatically. /health and static console assets remain unauthenticated;
the console cannot call protected routes without an external operator client.
Bearer comparison is constant-time, and structured access logs omit sensitive
headers.
Tide does not terminate TLS or use cookies. Public deployments must terminate TLS before the server and restrict origin ingress; bearer-only requests avoid a cookie-based CSRF boundary. There is still no tenant isolation or per-workflow authorization policy, so one server is a single-tenant operator plane.
Related
- Human-in-the-loop — the
hitl.wait/ resume modelresolvedrives. - CLI reference —
serveflags. - Driving Argon — embedded vs remote
--argon.