Thanks for considering a contribution. The OpenWOP v1.0 spec is small, mechanical, and intentionally focused — small PRs land fastest.
This guide covers:
1. What's in scope. 2. Status legend + when to bump status. 3. Per-artifact change rules (prose specs, JSON Schemas, OpenAPI, AsyncAPI, conformance, SDK). 4. The CI gate. 5. Coordination with the impl plan.
What's in scope
The openwop v1 corpus describes the wire-level contract between independent implementations of workflow orchestration servers and the clients that talk to them. It does NOT prescribe:
- Internal data structures (Zustand vs Redux vs raw classes — implementer's call).
- Storage backends (Firestore vs Postgres vs SQLite — implementer's call).
- How LLM prompts are constructed (implementer's call, modulo the
Capabilitieshandshake). - UI conventions (any UI is fine — the spec only defines the wire data).
When a PR proposes adding to one of those surfaces, expect pushback: it likely belongs in an implementation's docs, not the spec.
Status legend
Per auth.md §status legend (and reflected in every prose doc's header):
| Tag | Meaning |
|---|---|
| STUB | Minimal coverage of stable surfaces only. Implementers SHOULD pin only to what's documented; gaps are expected. |
| DRAFT | Comprehensive coverage of stable + in-flight surfaces, but not yet reviewed by spec committee. |
| OUTLINE | Sketched but not detailed. Section headings lock; field schemas may shift. |
| FINAL | Reviewed + frozen for a given v1.X release. Breaking changes require a major bump. |
When to bump status:
- STUB → DRAFT: when every stable wire-level field is documented (RFC 2119 keywords applied, examples present, edge cases called out).
- DRAFT → OUTLINE: backward — only when a section needs more design work than originally thought.
- DRAFT → FINAL: after committee review (none formally chartered yet — see "Process" below).
Per-artifact change rules
Prose specs (*.md)
- Every doc MUST include a header status block with: status tag, draft date, and a "stable surface for external review" note.
- Use RFC 2119 keywords (MUST, SHOULD, MAY, MUST NOT, SHOULD NOT) consistently.
- Cross-reference companion specs by relative path. From the repo root, use links like
capabilities.md; from insidespec/v1, link to peer docs by filename. - New surface area: add a "Why this exists" paragraph + an "Open spec gaps" table at the end.
- Normative examples are declared and validated (RFC 0149 §D). A fenced ``
json /`jsonc block inspec/v1/.mdthat is a WHOLE instance of a schema carries the markeron the line immediately above the fence.normative-example-extraction.test.tsextracts every declared example at test time and validates it againstschemas/ inverse* — a fenced block that validates as a whole instance of some schema but is not declared. Fragments (...with the same Ajv registration every other leg uses, and it also fails on the, jsonc comments, partial objects) are prose and take no marker; a declared example MUST be strict JSON. Discovery-shaped examples (rootprotocolVersion+supportedEnvelopes) MUST be declared againstcapabilities.schema.json` and MUST NOT carry credential- or tenant-shaped keys (RFC 0149 §E).
An RFC MUST NOT state a rule a core doc owns
Before landing any normative sentence in an RFCS/*.md, do two things by hand:
1. **Open the spec/v2/core/*.md that owns the rule and read what it already says. 2. Grep conformance/src/scenarios/** for a scenario asserting it.
openwop-check.sh does not cross-check an RFC's normative claims against the core spec or the suite. The corpus can hold a direct contradiction with every gate green, and has: RFC 0180 §A.4a asserted that an era-2 reader MUST accept a type the codemap does not name, while persistence.md §The reader rule, events.md §Era-2 and RFC 0176 §A.3 all require the opposite, and v2-unmapped-type-refused witnessed both halves of the opposite. It reversed a tracked migration decision (openwop.migration.C9.3) from inside a document about registration procedure, and was caught only when a host implemented it and would have turned a certified bundle red.
Ownership is the test. A reader rule belongs to RFC 0176 and persistence.md, not to whichever RFC happens to find a problem with it. When an RFC's argument is sound but the rule is not its to state, record the problem as an open question there and move the change to the owning doc — with the conformance scenarios moved in the same PR.
This is a process rule because it does not automate. Three mechanical proxies were designed and measured against the corpus, and all three fail:
| Proxy | Result |
|---|---|
| RFC asserts a MUST naming an error code, and the core doc owning that code does not cite the RFC | 367 findings, ~all noise — generic codes like validation_error appear in five core docs |
| Same, restricted to codes owned by 1–2 non-registry core docs | 35 findings, 0 real — a core doc absorbing a rule without citing the RFC number is normal |
| RFC asserting a MUST must name an existing conformance scenario | 0 signal — all 178 Active/Accepted RFCs already comply, §A.4a included |
The third is the instructive one: the defective RFC satisfied every structural proxy. It cited events.md. It named scenarios. The contradiction was semantic, and nothing in the corpus's shape distinguishes it from agreement.
JSON Schemas (schemas/*.schema.json)
- Every schema declares
$schema: "https://json-schema.org/draft/2020-12/schema". - Every schema has a
$idthat's a URL underhttps://openwop.dev/spec/v1/<name>.schema.json. - Use
additionalProperties: falseon every object — explicit field lists are mandatory for spec docs even if a runtime relaxes them. - New required fields: bump the schema's implicit minor version + update CHANGELOG.md. New optional fields are non-breaking.
Pack-internal JSON Schemas (packs/<name>/schemas/*.schema.json)
Moved: the pack ecosystem (
packs/source + theregistry/catalog) now lives inopenwop/openwop-registry. Pack contributions, the rules below, and the pack/registry validation gate (scripts/registry-check.sh) apply in that repo. The pack-manifest _schemas_ remain normative here underschemas/(vendored into the registry repo with a drift guard).
Distinct from the spec-corpus schemas above. These live inside a pack's tarball and are referenced via pack.json (configSchemaRef / inputSchemaRef / outputSchemaRef for nodes; handoff.{task,return}SchemaRef for agent manifests per RFC 0003 §D). Rules:
$schema: "https://json-schema.org/draft/2020-12/schema"— same as spec corpus.$idMUST use the registry-canonical, version-bearing form:https://packs.openwop.dev/<pack-name>/<version>/<file-name>.schema.json. The version segment MUST matchpack.json.version. This keeps$idimmutable across pack version bumps — caching tools that key on$idsee distinct documents per pack version (per JSON Schema 2020-12$idimmutability semantics).additionalProperties: falseon every object — same discipline as spec corpus.- Pack
versionbump: regenerate every$idin the pack'sschemas/directory to the new version. The registry repo'sscripts/check-pack-schema-ids.mjsenforces this (run locally vianpm run check, and inpacks-checkCI);--fixregenerates them. Correction (2026-08-05): this line previously saidscripts/precheck-packs.mjsSHOULD catch drift — it does not check$idat all, and the only enforcement lived in an inline heredoc inside a CI workflow, so it could not be run locally. A routine version bump then leftmainred for four days.
OpenAPI / AsyncAPI
- Reference JSON Schemas via cross-file
$ref(../schemas/<name>.schema.json); never inline. - Lint must pass:
redocly lint api/openapi.yamlandasyncapi validate api/asyncapi.yamlfrom@asyncapi/cli. - Bundle must succeed:
redocly bundle api/openapi.yamlandasyncapi bundle api/asyncapi.yaml. - New endpoints: add a
tag, anoperationId, request/response schemas, and at least one error response.
Conformance suite (conformance/)
- Each new scenario file in
conformance/src/scenarios/follows the existing pattern:
- Top-of-file docstring stating the spec doc(s) being verified. - describe('category: …', …) blocks per assertion group. - expect(…, driver.describe('spec.md §section', 'requirement')) so failure messages cite the requirement.
- New fixtures go in
conformance/fixtures/AND must be added tofixtures.md's catalog table + per-fixture contracts. Thespec-corpus-validity.test.tsround-trip test will fail otherwise. - Server-free scenarios (those not requiring
OPENWOP_BASE_URL) MUST run in <1s. CI gates on this. - Never return early in silence (RFC 0148 §A). A test that returns before its first
expectis a pass with zero assertions — an unclassified return. Gate a profile withbehaviorGate(profile, advertised)(recordsinapplicable/skippedand fails strict mode on an advertised-missing seam); for every other early return say why withsoftSkip(kind, reason)—return softSkip('inapplicable', 'host does not advertise X')— orreturn seamAbsent(reason)when the host advertises a capability but the seam answers 404/403 (blockedin default mode, a failure underOPENWOP_REQUIRE_BEHAVIOR=true; a 403 is not a pass). The runner records a zero-assertion file with no note asblockedwith a fixed marker (UNCLASSIFIED_RETURN_DETAIL), which certification still treats as unclassified — the bundle row is honest, and the pressure to say why survives. If you skip with vitest'sctx.skip(), write the note first:ctx.skip()throws, so asoftSkip(...)placed after it is dead code and the file reports as unclassified anyway (seven files carried exactly that for a suite minor).
- Before writing a scenario, ask whether the condition is CAUSABLE, not just whether the property is OBSERVABLE. These come apart constantly and the failure is silent, because a scenario built on the wrong half still runs and still goes green. RFC 0158's
duplicate-deliveryis the worked example: duplicate effects are perfectly observable, but §C.7 is about a host's queue redelivering accepted work, and the suite cannot make a queue redeliver — a same-Idempotency-Keyretry witnesses Layer-1 client dedup instead, a different mechanism wearing the same name. Between two sessions this distinction produced four wrong scenario designs in one day, always in the same direction: the property was checkable, the condition was not, and the scenario ended up asserting something adjacent while reading as coverage. If the condition needs a host seam, an operator precondition, or a process death, say so and gate on it — do not substitute the nearest causable thing. - When a conforming host FAILS and a lenient one PASSES, suspect the oracle before the host. The default reflex runs the other way, and the default reflex was wrong here.
webhook-signed-delivery.test.tsrequired asubscriptionIdfield thatapi/openapi.yamldoes not define andwebhooks.mddoes not show — so a host implementing only the published contract failed at the first assertion, while the postgres reference host passed because it returns both names, one labelled a historical alias in its own source. A reference implementation's compatibility shim hid a contract error from everyone who was not equally lenient, for as long as the shim existed. Surfaced by a tier-2 host's failing conformance run; diagnosed by a tier-1 host session that went looking for why nothing had ever reddened. The corollary for reference hosts: a shim that accepts more than the contract costs the suite its only signal — if you add one, add a test that the contract shape is what the scenario asserts. - A wrong comment that cites its authority is worse than a wrong comment that doesn't. Formulated by a tier-2 host after finding that its webhook signature prefix had been deliberately removed on a misreading of
webhooks.md, and the misreading then written into the signing module's docblock citing that spec section as its justification: "a comment that cites a source it contradicts is worse than no comment — it converts a mistake into a documented decision." An unsupported wrong comment is a mistake the next reader can catch by checking. A wrong comment that names its authority is armoured — the reader follows the citation, finds a real section, and stops there. It defends the error with the artifact that refutes it. So when you cite a spec section in a comment, quote the clause you are relying on, and when you correct such a comment, record that the citation was wrong rather than silently repointing it. - When you sabotage-check a scenario, verify the sabotage is PRESENT IN THE TREE before you trust the result. A sabotage that silently fails to apply is indistinguishable from a test that fails to catch — both produce a green, and the green is the thing you are looking at. This nearly published a false negative: a patch script asserted a unique match on a string that occurred three times, threw, never applied, and the subsequent rebuild-and-run passed against unmodified code — one step from reporting "the scenario does not catch this", which was the opposite of the truth.
grepfor the injected marker, or diff the built artifact, and only then run. Sabotage verification needs its own positive control, for exactly the reason the scenario under test needs one.
TypeScript reference SDK (sdk/typescript/)
> Moved: the three reference SDKs (TypeScript, Python, Go) now live in openwop/openwop-sdks; SDK contributions and the SDK build/lint/parity gate (scripts/sdks-check.sh) apply in that repo. The rules below are retained because they remain the contract a spec change imposes on the SDKs.
- Every endpoint in
api/openapi.yamlshould map to ONE method onOpenwopClient. If you add an endpoint to the spec, add the corresponding SDK method in a pairedopenwop-sdksPR. - Types come from the spec — extend
src/types.tsrather than redefining shapes inline. tsc --noEmitmust pass withstrict + exactOptionalPropertyTypes. Noas any, no@ts-ignore.- Zero runtime dependencies remains a goal. New deps need a stated reason in the PR description.
Reference applications (apps/)
Moved: the deployable reference app was extracted to
openwop/openwop-app, and the single-file demos + conformance-target hosts toopenwop/openwop-examples. Contributions to either go to those repos; the conventions below travel with them.
A separate tier from the single-file demos and examples/hosts/ conformance-test targets (both in openwop-examples). Each reference app is a deployable template — backend + frontend + Dockerfile + auth + storage + observability wired together.
Conventions when contributing a new sample (or a new BE/FE under an existing one):
- Layout. Backends live under
apps/<sample>/backend/<language>/; frontends underapps/<sample>/frontend/<framework>/. Mirror an existing sample's structure. - Boundary discipline. Anything sample-specific (stub auth, demo packs, local workflows) MUST live under
local.orsample.namespaces — never undercore.,openwop., orvendor.<org>.*. Thehost-extensions.mdnamespace rule applies insideapps/exactly as it does in production hosts. - Honest discovery. The
/.well-known/openwopadvertisement MUST reflect what the sample actually implements. Do not claim profiles or capabilities that are stubbed — downgrade the advertisement instead. - Banned patterns. No
as any,@ts-ignore, or@ts-nocheckinapps/<sample>/<lane>/<language>/src/. Production-grade hygiene applies even though samples are non-normative — the sample teaches by example. - Dependencies. Sample dependencies are local to the sample; root
npm installand the spec corpus are unaffected. New deps need a one-line justification in the PR. - CI. Sample CI runs in its own workflow step. A sample failure does NOT block a spec release; the gate stays scoped to spec/SDK/conformance/security.
- Public docs. Mention the sample in the root
README.md"Reference applications" section + add a[Unreleased]line inCHANGELOG.md. Do NOT introduce deployment-target language ("Cloud Run", "AWS Lambda", "Fly.io") intoREADME.md/QUICKSTART.md—spec-corpus-validity.test.tsenforces neutrality there. The sample's own README is the place for deployment specifics. - Documentation. Each
apps/<sample>/has aREADME.md(run instructions + honest pass-matrix) andARCHITECTURE.md(component map + boundary discipline). The latter cites where each spec requirement is implemented in the sample.
The CI gate
A openwop-spec PR is mergeable when:
1. redocly lint api/openapi.yaml — clean. 2. asyncapi validate api/asyncapi.yaml — clean. 3. Every JSON Schema compiles via Ajv2020 (covered by conformance/src/scenarios/spec-corpus-validity.test.ts). 4. Every fixture validates against workflow-definition.schema.json (covered by conformance/src/scenarios/fixtures-valid.test.ts). 5. Every prose doc carries a Status: legend tag (covered by spec-corpus-validity.test.ts). 6. The openwop-conformance --offline server-free subset passes. 7. CHANGELOG.md updated when changing any artifact (1-line entry under [Unreleased] is fine). 8. Every commit on the PR carries a Signed-off-by: trailer per the DCO (see §"Sign your commits" below).
(The SDK build + per-SDK lint gates moved to openwop-sdks with the SDKs — that repo's scripts/sdks-check.sh covers TypeScript tsc/ESLint, Python ruff, Go go vet/gofmt, and cross-SDK parity.)
Run the full local check from the repo root:
npm run openwop:check
Equivalent direct script:
bash scripts/openwop-check.sh
Optional pre-commit guard
Install once per clone to catch the most common author-side slip (staging an RFC change without regenerating docs/PROTOCOL-STATUS.md and README.md):
bash scripts/install-git-hooks.sh
This symlinks scripts/hooks/pre-commit into .git/hooks/. The hook is fast (<1s) and only fires when staged paths match RFCS/*.md. Heavier validation stays in CI.
Coordination with the impl plan
When a spec PR proposes a change that interacts with a reference implementation:
- Cosmetic / additive (new field, new event type as opt-in, new endpoint): merge spec PR independently. Impl will catch up.
- Breaking impl assumptions (schema bump on existing event, new required field, removed field): coordinate via
WORKFLOW-PROTOCOL-openwop-PLAN.md"Cross-cuts to impl plan" section. Add aCC-Nentry. The impl plan owner approves before merge.
Cross-cuts currently tracked: CC-1 (recursionLimit invariant — partial), CC-2 (typed channels — deferred), CC-3 (OTel taxonomy — done), CC-4 (maxNodeExecutions — done).
Process
The openwop spec doesn't yet have a formal committee. Until one exists:
- PRs: opened against the implementation repo, labeled
openwop-spec. Merge bar is "two reviewers from different organizations" once the spec leaves DRAFT. - Issues: see
README.md§Reporting issues — include doc filename, section heading, RFC 2119 requirement that's unclear or contradictory, and implementation impact. - Backwards compat: until v1 FINAL, breaking changes are allowed but MUST come with a CHANGELOG entry + a runbook section in
version-negotiation.mddescribing migration.
Sign your commits (DCO)
Every commit on a pull request MUST carry a Signed-off-by: trailer. This is the Developer Certificate of Origin — the lightweight alternative to a CLA. By signing off, you assert you have the right to submit the work under the project's license (Apache-2.0 for code, CC-BY-4.0 for spec text).
How to sign:
git commit -s -m "your message" # adds Signed-off-by automatically
git commit --amend -s --no-edit # add to an existing commit
git rebase --signoff -i HEAD~3 # add to the last 3 commits
The DCO check is wired through the DCO bot; it runs on every PR and blocks merge until every commit is signed off. A failing DCO check is the only "fix-forward" the maintainer set explicitly allows: amend + force-push and we'll re-run.
Bootstrap-phase reality (until MAINTAINERS.md lists a non-steward maintainer): the steward currently lands commits directly on main without a PR gate, so the DCO bot does not run on those commits in practice. Commits authored in bootstrap phase have shipped with Co-Authored-By: trailers (for AI-assistant attribution) but without Signed-off-by:. This is a documented drift between the stated rule and current practice. The MUST above becomes operationally enforceable once the PR-based workflow re-engages — i.e., as soon as a non-steward maintainer joins per the ROADMAP.md migration tripwire. Until then, contributors submitting PRs SHOULD sign their commits; the steward's direct-to-main commits are exempt by practice but the exemption is recorded here for transparency.
Triage SLA
A maintainer will respond to your PR or issue within:
- 24 hours for security-flagged issues (per
SECURITY.md). - 7 calendar days for everything else.
"Respond" means substantive: a review, a redirect, or a "I'll get to this by ~date." Silence past 7 days means the maintainer rotation isn't keeping up; ping @davidscotttufts directly.
If your PR sits past 14 days without a substantive response, that's a maintainer-set capacity problem, not a quality problem with your contribution. We document this honestly so contributors can decide whether to wait.
Bootstrap-phase notes (2026-05-05)
Until MAINTAINERS.md lists at least one maintainer not affiliated with the original steward (per the ROADMAP.md migration tripwire), the following bootstrap-phase rules apply:
- One-approval review. Branch-protection on
mainrequires one maintainer approval. Post-bootstrap (when MAINTAINERS.md grows past one), this becomes two approvals from different organizations perGOVERNANCE.md§"Decision making." - Conformance scenario authorship. PRs touching
conformance/src/scenarios/orconformance/src/lib/route throughCODEOWNERSto the lead maintainer. Same elevation post-bootstrap (cross-org reviewers required). - Spec corpus changes. Same elevation logic —
CODEOWNERSroutes/spec/v1/,/api/,/schemas/to the lead maintainer; cross-org review post-bootstrap. - DCO
Signed-off-by:exemption. The steward's direct-to-maincommits ship withoutSigned-off-by:trailers in practice (see §"Sign your commits (DCO)" above). External contributors submitting PRs MUST still sign every commit. The exemption ends when the PR-based workflow re-engages with the first non-steward maintainer. bash scripts/openwop-check.shbefore push. The 8-step gate is fast (~30s warm cache) and surfaces fixture-catalog drift, schema discipline breaks, and conformance-validity issues before they hitorigin/main. Run it before every push to avoid red gates onmainthat block other contributors.- RFC comment-window waivers. Additive RFCs (7-day window) MAY be promoted Draft → Active by steward decision when the comment window would only serve as a delay against zero external reviewers. Each waived RFC MUST record the waiver in its
Updatedfield. RFCs 0009 and 0010 are the worked examples — seeMAINTAINERS.md§"Bootstrap-phase RFC waivers" for the running list.
The bootstrap-phase amendment is filed as RFC 0005 in the RFCS/ directory.
Useful one-liners
# Validate every schema compiles + fixtures + spec corpus, all server-free
# (build the CLI once first: cd conformance && npm install && npm run build:cli)
conformance/dist/cli.js --offline
# Lint OpenAPI (pinned — matches the openwop-check.sh gate; @latest races the npm cache)
npx -y -p @redocly/cli@2.31.4 redocly lint api/openapi.yaml
# Validate AsyncAPI (pinned — 4.1.1 is the last Node-22-compatible release)
npx -y -p @asyncapi/cli@4.1.1 asyncapi validate api/asyncapi.yaml
# Build the TS SDK (lives in openwop-sdks now)
(cd ../openwop-sdks/sdk/typescript && npm install && npm run build)
# Find every prose doc that's still STUB-tier (candidates for promotion)
grep -l "Status:.*STUB" *.md