Status: v2. Every call below uses unversioned paths (
/runs) and sends anOpenWOP-Version: 2header. The responses were captured from the v2 reference host and abbreviated. The capture ran on port 3990; your host listens on 3838, so its URLs will say3838.
This is the fastest path from "what is OpenWOP?" to "I have a v2 workflow running on my laptop". You need Node 20+ and a clone of openwop/openwop-examples, which holds the reference hosts and runnable samples. You don't need a vendor SDK, a managed service, or a framework.
- Minute 0–2: start the v2 reference host.
- Minute 2–5: run a workflow with curl.
- Minute 5–8: run the same workflow with the TypeScript SDK.
- Minute 8–10: stream events live over SSE, resume a stream, and cancel a run.
QUICKSTART.md covers more (webhooks, fork and replay, configurable, node packs, conformance) and works against any v2 host. This guide assumes you have nothing yet.
Prerequisites
node --version # must be >= 20
git --version
That's all. You don't need Docker, a cloud account, or provider API keys.
Minute 0–2: start the v2 reference host
git clone https://github.com/openwop/openwop-examples.git
cd openwop-examples/examples/hosts/v2-reference
npm install --legacy-peer-deps
npm start
You should see a line like this one (captured with OPENWOP_PORT=3990; yours names port 3838 and db data/v2-reference.sqlite):
openwop-host-v2-reference listening on http://127.0.0.1:3990 (protocolVersions 1.11, 2.0; preferredVersion 1.11; db …; fixtures 16; seams mounted; spec-artifacts 2.42.2)
Leave it running. --legacy-peer-deps is required: the host's README explains the exact-pinned conformance peers it works around. The host stores runs in one SQLite file (data/v2-reference.sqlite). Set OPENWOP_DB_PATH=:memory: if you want nothing written to disk.
The boot line says protocolVersions 1.11, 2.0; preferredVersion 1.11: this host still serves the frozen v1 wire next to v2. An unversioned path such as /runs is always the v2 surface. Only /.well-known/openwop follows the header, and without one it returns the preferredVersion (v1) document (spec/v2/core/versioning.md §1.3). Every call below sends OpenWOP-Version: 2 so discovery returns the v2 document too.
In a separate terminal:
export OPENWOP_URL=http://127.0.0.1:3838
export TOKEN=openwop-v2-dev-key # the host's default dev key (OPENWOP_API_KEY overrides it)
Minute 2–5: run a workflow with curl
Discover
curl -s -H 'OpenWOP-Version: 2' $OPENWOP_URL/.well-known/openwop \
| jq '{protocolVersions, preferredVersion, implementation, replay}'
{
"protocolVersions": ["1.11", "2.0"],
"preferredVersion": "1.11",
"implementation": {
"name": "openwop-host-v2-reference",
"version": "2.0.0-rc.1",
"vendor": "openwop (reference example)"
},
"replay": {
"status": "experimental",
"since": "2.0",
"until": "2.1",
"witness": "witnessable-gated",
"modes": ["replay", "branch"],
"retention": { "days": 30 },
"effectSeamsManifest": "/host/effect-seams"
}
}
This is the closed v2 discovery root. Each family (replay here) is a record with status, since, witness, and its facets. A family that is present is supported. v2 has no supported: true flags. jq '.fixtures' lists the 16 test workflows the host can run.
Create a run
curl -s -X POST $OPENWOP_URL/runs \
-H 'OpenWOP-Version: 2' \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"workflowId":"conformance-multi-node"}'
{
"runId": "openwop-reference-tenant/9fasFA1XO6OIVVrEDg6S_Msv",
"status": "pending",
"eventsUrl": "http://127.0.0.1:3990/runs/openwop-reference-tenant~2F9fasFA1XO6OIVVrEDg6S_Msv/events",
"statusUrl": "http://127.0.0.1:3990/runs/openwop-reference-tenant~2F9fasFA1XO6OIVVrEDg6S_Msv"
}
A v2 runId is tenant-bound (<tenant>/<opaque>). In a URL, the / travels as ~2F. Use the last path segment of statusUrl as your run reference:
RUN=openwop-reference-tenant~2F9fasFA1XO6OIVVrEDg6S_Msv # paste yours
Get the snapshot
curl -s -H 'OpenWOP-Version: 2' -H "Authorization: Bearer $TOKEN" \
$OPENWOP_URL/runs/$RUN | jq '{status, owner, eventLogSchemaVersion}'
{
"status": "completed",
"owner": {
"tenant": "openwop-reference-tenant",
"subject": {
"issuer": "urn:openwop-host-v2-reference:api-key",
"subjectId": "default",
"tenant": "openwop-reference-tenant",
"lane": "api-key",
"kind": "user"
}
},
"eventLogSchemaVersion": 3
}
You just ran an OpenWOP v2 workflow with three HTTP calls and no client library. owner.subject is who the host decided you are, and the host derives it from your credential, never from the request body.
Minute 5–8: run the same workflow with the TypeScript SDK
The 2.x line of @openwop/openwop sends OpenWOP-Version on every request.
mkdir -p /tmp/openwop-quickstart && cd /tmp/openwop-quickstart
npm init -y > /dev/null
npm pkg set type=module
npm install @openwop/openwop@2
cat > quickstart.mjs <<'EOF'
import { OpenwopClient } from '@openwop/openwop';
const client = new OpenwopClient({
baseUrl: 'http://127.0.0.1:3838',
apiKey: 'openwop-v2-dev-key',
});
const caps = await client.discovery.capabilities();
console.log('Server:', caps.implementation?.name, caps.protocolVersions);
const { runId } = await client.runs.create(
{ workflowId: 'conformance-multi-node' },
{ idempotencyKey: crypto.randomUUID() },
);
console.log('Created:', runId);
for await (const event of client.runs.events(runId)) {
console.log(` [${event.sequence}] ${event.type}`);
}
const snap = await client.runs.get(runId);
console.log('Final:', snap.status);
EOF
node quickstart.mjs
Output (captured with @openwop/openwop 2.3.0):
Server: openwop-host-v2-reference [ '1.11', '2.0' ]
Created: openwop-reference-tenant/G_N8_vAoLjSAzDK7a2rLAB-n
[0] run.started
[1] node.started
[2] node.completed
[3] node.started
[4] node.completed
[5] node.started
[6] node.completed
[7] run.completed
Final: completed
client.runs.events consumes the SSE stream. It ends after the terminal event, so you don't need a polling loop. The Python (openwop-client>=2) and Go (github.com/openwop/openwop-sdks/go/v2) clients have the same shape. All three live in openwop/openwop-sdks.
Note: to test an unpublished SDK checkout instead, clone
openwop-sdks, runnpm install && npm run build && npm linkinsdk/typescript-v2/, and replace the install step withnpm link @openwop/openwop.
Minute 8–10: stream events live over SSE
Start a long-running workflow. conformance-cancellable waits delayMs before it completes.
curl -s -X POST $OPENWOP_URL/runs \
-H 'OpenWOP-Version: 2' -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"workflowId":"conformance-cancellable","inputs":{"delayMs":30000}}'
RUN=... # the last path segment of the statusUrl it returns
Attach to its event stream:
curl -s -N -H 'OpenWOP-Version: 2' -H "Authorization: Bearer $TOKEN" \
"$OPENWOP_URL/runs/$RUN/events"
While it waits, cancel it from a third terminal:
curl -s -X POST -H 'OpenWOP-Version: 2' -H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' -d '{"reason":"quickstart demo"}' \
"$OPENWOP_URL/runs/$RUN/cancel"
{"runId":"openwop-reference-tenant/1xe1dnUaKrvDQjragfK7I05a","status":"cancelling"}
The stream shows the whole life of the run and then closes:
id: 0
event: run.started
data: {"eventId":"nujxvYrr4atcpEqbUTS0Dls6","runId":"openwop-reference-tenant/1xe1dnUaKrvDQjragfK7I05a","type":"run.started",…}
id: 1
event: node.started
data: {…,"type":"node.started","payload":{"nodeId":"wait","typeId":"core.delay",…}}
id: 2
event: node.cancelled
data: {…,"type":"node.cancelled",…}
id: 3
event: run.cancelled
data: {…,"type":"run.cancelled","payload":{"reason":"quickstart demo",…}}
Three details are worth knowing:
id:is the event'ssequence. If your connection drops, reconnect with-H 'Last-Event-ID: 1'and the host sends only events with a higher sequence.- The host replays the backlog on connect. Attaching after a run has finished still shows every event, and the stream closes after the terminal event.
- A second cancel is refused. The run is already terminal, so you get
409 run_terminalwithdetails.runStatus: "cancelled".
What you just learned
| Concept | Where to read |
|---|---|
| The v2 wire contract | spec/v2/core/overview.md (reading order), api/v2/openapi.yaml |
Version negotiation, OpenWOP-Version | spec/v2/core/versioning.md §1 |
| Discovery records | spec/v2/core/capabilities.md |
| Run lifecycle, snapshot, cancel | spec/v2/core/runs.md |
| Events, SSE, poll | spec/v2/core/events.md |
| Tenant-bound ids, Subjects | spec/v2/core/identity.md |
| Idempotency | spec/v2/core/idempotency.md |
| Profiles | spec/v2/profiles.json, capabilities.md §7 |
| Build your own host | examples/hosts/v2-reference/, the host you just ran. It is implemented from spec/v2/core/, and its src/ layout is in its README |
| Conformance | QUICKSTART.md §8: npx openwop-conformance --base-url $OPENWOP_URL --api-key $TOKEN --target-major 2 |
Where to go next
QUICKSTART.mdcovers webhooks, fork and replay,configurable, node packs, conformance, and SDKs against any v2 host.INTEROP-MATRIX.mdshows which hosts have a v2 certification bundle, including this one.docs/migration/v1-to-v2.mdis for readers who have v1 client code.- Build a node pack:
docs/PACK-AUTHOR-QUICKSTART.md.
The tiny-workflow and streaming-client samples in openwop-examples speak v2 and run against the v2 reference host. The in-memory host there serves only the frozen v1 wire (/v1/…, port 3737), so it won't work with this walkthrough.
Troubleshooting
| Issue | Fix |
|---|---|
EADDRINUSE on port 3838 | Run OPENWOP_PORT=3990 npm start and change OPENWOP_URL to match. |
npm install fails resolving peers | Use npm install --legacy-peer-deps. If npm 10.9 crashes (edgesOut), use a current npm: npx -y npm@latest install --legacy-peer-deps. |
401 unauthenticated | Include Authorization: Bearer openwop-v2-dev-key, or whatever you set OPENWOP_API_KEY to. |
400 idempotency_key_invalid | Idempotency-Key must match ^[A-Za-z0-9._~-]{22,128}$. Use a UUID (uuidgen). |
400 validation_error naming an unknown key | The POST /runs body is closed. Fields it does not define, such as a free-form configurable map, are refused. See runs.md §Create. |
403 mentioning tenantId | Leave tenantId out. The tenant comes from your credential. |
400 protocol_version_mismatch | You sent OpenWOP-Version: 2 to a /v1/… path. Drop the /v1 prefix. |
Discovery returns the v1 document (OpenWOP-Version: 1.11 on the response) | Send the OpenWOP-Version: 2 request header. Without it, discovery follows the host's preferredVersion, which is 1.11 on this host. |
If something else doesn't work, file an issue at <https://github.com/openwop/openwop/issues>. The v2 reference host is supposed to "just work" for this guide.