OpenWOP openwop.dev

Status: Draft v1.x (filed via RFC 0027, 2026-05-19; first cut 2026-05-20). Lands the wire shape for portable, versioned, variable-bound prompts referenced by workflow nodes and agent manifests. Closes the gap where core.ai.callPrompt config (workflow-chain-packs.md line 69, host-capabilities.md line 347) and AgentManifest.systemPrompt | systemPromptRef (agent-manifest.schema.json lines 34–41) accept inline prompt bodies but offer no shared addressing, library distribution, variable schema, or observability of the composed result. Keywords MUST, SHOULD, MAY follow RFC 2119. See auth.md for the status legend. Fields marked (stable) lock; fields marked (in-flight) may shift compatibly within v1.x.


Why this exists

Two v1 surfaces already accept prompt bodies, but neither establishes prompt-as-a-resource:

  • workflow-chain-packs.md line 71 demonstrates core.ai.callPrompt config carrying "systemPrompt": "You are a senior PM. Write a PRD for: {{params.productIdea}}\nAudience: {{params.targetAudience}}". The {{params.X}} substitution is convention-only — no spec'd variable schema, no enumeration of allowed sources, no observability of the composed result.
  • schemas/agent-manifest.schema.json lines 34–41 + 104–105 lock systemPrompt XOR systemPromptRef. Both are tarball-resident; neither can be referenced across packs or replaced at run time.

Authors who want canvas-editor patterns of (a) maintain a named, versioned prompt library, (b) reuse the same prompt across multiple nodes or workflows, (c) preview the composed body before dispatch, or (d) audit what every agent actually saw in a multi-agent run have no protocol-level surface for any of these. Host-internal libraries exist (the reference myndhyve impl persists PromptEntry/PromptLibrary documents in Firestore at workspaces/{workspaceId}/prompt-libraries/{canvasTypeId} with built-in/override/version semantics), but the values they hold cross the wire as opaque interpolated strings — losing the ID, the version, the variable schema, and the resolution chain.

This document closes the wire-shape gap. Phase A only — the shape, the capability flag, and the observability event. RFC 0028 lands the registry surface (/v1/prompts/* endpoints + kind: "prompt" pack distribution); RFC 0029 lands the agent-scoped resolution hierarchy + agent.promptResolved event.


What a Prompt Template is (and is not)

A PromptTemplate is openwop's canonical wire format for a named, versioned, variable-bound prompt body. A PromptTemplate is a single JSON document whose top-level shape is fixed by schemas/prompt-template.schema.json, whose kind is selected from the shared prompt-kind.schema.json enum, whose text field MAY contain Mustache-compatible {{varName}} placeholders, and whose variables[] array typed-declares each placeholder.

A PromptTemplate is distinct from AIEnvelope (ai-envelope.md) and distinct from RunEventDoc (run-event.schema.json):

ConcernPromptTemplateAIEnvelopeRunEventDoc
DirectionAuthored — host library, pack-distributedInbound — LLM → engineOutbound — host → client
Source of truthHost library (built-in + installed packs + user)Single emission, recorded as RunEventDocAppend-only run event log
Type discriminatorkind ∈ {system, user, few-shot, schema-hint}Open-ended kind catalog, host-advertisedFixed 51-variant enum, FINAL v1
LifecycleAuthored once; resolved at every node executionValidated → gated → routed → recordedImmutable after appendAtomic
AudienceWorkflow editors, node dispatcherEngine, node dispatcherClients, observability, replay

In short: PromptTemplates are what the host sends to the LLM. AIEnvelopes are what the LLM sends back. RunEventDocs are what the engine reports to clients.

When a host composes a PromptTemplate for an LLM call (per the resolution chain in RFC 0029), it MAY emit a prompt.composed RunEventDoc capturing the composed bodies, refs, variable bindings, and content-trust marker — see §"Composition + observability" below.


PromptKind

schemas/prompt-kind.schema.json (NEW) holds the shared kind enum referenced by every schema that names a prompt kind. The enum has four values:

KindComposed asNotes
systemLLM system messageCarries behavior + output discipline. Composed once per call.
userLLM user messageThe variable-substitution surface — per-call task content.
few-shotUser-message prefix (or alternating user/assistant turns per host policy)Example bodies. Hosts MAY support multiple few-shot templates per node via additionalPromptRefs.
schema-hintInjected into system or user message at compose time (per host policy)Structured-output schema description (e.g., the JSON Schema the LLM is asked to populate).

A node MAY reference one template of each kind via the WorkflowNode.config.{systemPromptRef, userPromptRef, fewShotPromptRefs, schemaHintPromptRef} convention — defined normatively in §"Resolution chain" Layer 1 below and in the config description of schemas/workflow-definition.schema.json.

Adding a new kind in a future RFC is a single edit to prompt-kind.schema.json; consumers automatically pick it up. Hosts MAY narrow the accepted kinds via capabilities.prompts.templateKinds[].


PromptTemplate

The canonical wire shape — see schemas/prompt-template.schema.json for the full schema. Required fields:

interface PromptTemplate {
  templateId: string;     // ^[a-z0-9][a-z0-9._-]{0,127}$
  version: string;        // SemVer 2.0.0
  kind: PromptKind;       // shared enum, see above
  text: string;           // <= 65536 bytes
}

Optional fields: name, description, variables, modelHints, tags, meta.

Variable interpolation

The text body uses Mustache-compatible {{varName}} placeholders. No control-flow logic — substitution is purely literal. Hosts MUST:

  • Resolve each {{varName}} against the bound value (from variables[].source).
  • Fail the node with prompt_variable_unresolved when a required variable has no binding.
  • Render unresolved optional variables as the empty string (the onUnresolved: 'empty' semantics from the myndhyve reference impl).

A {{varName}} placeholder that has no matching entry in variables[] MAY appear (treated as an optional variable with source input and no default). Hosts SHOULD warn at install time when a placeholder lacks a corresponding declaration.

PromptVariable

interface PromptVariable {
  name: string;           // ^[a-zA-Z_][a-zA-Z0-9_]{0,63}$
  type: 'string' | 'number' | 'boolean' | 'array' | 'object';
  required: boolean;
  source?: 'input' | 'variable' | 'secret' | 'context';  // default: 'input'
  extractPath?: string;   // JSONPath into the source
  defaultValue?: unknown; // used when required: false and source resolves to undefined
  description?: string;
}

The source value determines where the binding comes from:

  • input (default) — the node's input port whose name matches name.
  • variable — a run-scoped variable resolved via ctx.variables.get(name) (or extractPath).
  • secret — a BYOK secret reference. MUST be redacted to [REDACTED:<secretId>] markers in any observability output (prompt.composed events, debug bundles) per SECURITY/threat-model-secret-leakage.md §SR-1.
  • context — a host-provided implicit context value (canonical names recommended below; hosts MAY define vendor-specific keys).

To improve cross-host portability of templates that use the context source, hosts SHOULD recognize these canonical names where the underlying value exists:

  • currentUserId — the authenticated user dispatching the run.
  • runId — the current run identifier.
  • workflowId — the workflow being executed.
  • workflowName — the human-readable workflow name.
  • tenantId — the tenant scope (when the host supports multi-tenancy).
  • nodeId — the executing node's id.
  • now — ISO 8601 UTC timestamp at composition time.

Hosts MAY surface additional context keys; templates that depend on host-specific keys lose cross-host portability.


PromptRef

A PromptRef is the reference type that workflow-node config and agent-manifest fields carry to point at a PromptTemplate. Two equivalent forms (see schemas/prompt-ref.schema.json):

Stringy form — canonical for inline use in WorkflowNode.config:

prompt:writer-system@1.0.0
prompt:vendor.acme.writer.v2
prompt:critic-system

Pattern: ^prompt:<templateId>(@<version>)?$. When version is omitted, the host resolves the latest version available.

Object form — canonical when libraryId disambiguation, per-reference variable overrides, or version pinning need to be explicit:

{
  "libraryId": "vendor.acme.editorial-prompts",
  "templateId": "writer-system",
  "version": "1.0.0",
  "variableOverrides": { "tone": "formal" }
}

variableOverrides apply at composition time and take precedence over node-input bindings (matching the resolution chain in RFC 0029 §A).

Conflict resolution

When two installed packs ship the same templateId, the stringy form is rejected with prompt_ref_ambiguous and consumers MUST use the object form with explicit libraryId to disambiguate (per RFC 0028 §B).


Capability advertisement

A host advertises its prompt-resolution support via capabilities.prompts (per capabilities.schema.json). The block carries two independent axes — node-execution PromptRef resolution (Phase A) and the /v1/prompts* REST surface (Phase B) — each gated by its own flag so a host can implement either without the other.

{
  "prompts": {
    "supported": true,
    "endpointsSupported": false,
    "templateKinds": ["system", "user", "schema-hint"],
    "variableSources": ["input", "variable", "context"],
    "maxTemplateBytes": 16384,
    "observability": "full"
  }
}

Field semantics:

FieldRequiredSemantics
supportedyesRFC 0027 Phase A gate. When true, the host resolves PromptRef values on WorkflowNode.config.{systemPromptRef, userPromptRef, additionalPromptRefs} at node-execution time and emits prompt.composed events. When false or absent, those keys are treated as opaque strings and never composed. **Does NOT imply the /v1/prompts* REST surface is available** — see endpointsSupported.
endpointsSupportednoRFC 0028 Phase B gate. When true, the host serves the /v1/prompts REST surface (at minimum the read endpoints). When false or absent, every /v1/prompts request returns 501 capability_not_provided. Independent of supported.
templateKindsnoSubset of PromptKind values the host accepts. Default: all four.
variableSourcesnoSubset of PromptVariable.source values supported. secret SHOULD only appear when capabilities.secrets.supported: true.
maxTemplateBytesnoHost cap on text length. MUST NOT exceed the schema cap (65536).
observabilitynooff / hashed / full — controls prompt.composed emission per §"Composition + observability" below. Default: hashed.

Phase B (RFC 0028) extends this block with packsSupported (pack-install path; requires endpointsSupported: true to be meaningful), mutableLibrary (write endpoints; requires endpointsSupported: true), and library (per-library knobs). Phase C (RFC 0029) extends it with defaults and agentBindings. This document covers Phase A; the Phase B fields are documented in §"Discovery & distribution" below.


Composition + observability

When a node executes and the host has resolved a PromptRef (per the resolution chain in RFC 0029), the host composes the prompt body by:

1. Substituting {{varName}} placeholders against the resolved variable bindings. 2. Wrapping any input flagged meta.contentTrust: "untrusted" (per mcp-integration.md §"Trust boundary" + RFC 0020 §D) in <UNTRUSTED>...</UNTRUSTED> markers — the markers MUST be preserved verbatim into the composed body per SECURITY/threat-model-prompt-injection.md. 3. Replacing any secret-sourced variable values with [REDACTED:<secretId>] markers in any observability projection (the markers do NOT appear in the actual dispatched body — the host resolves real values via capabilities.secrets after redaction-projection).

When the host advertises capabilities.prompts.observability !== "off", the host MUST emit a prompt.composed RunEventDoc per node composition:

interface PromptComposedPayload {
  nodeId: string;
  refs: string[];              // ["prompt:writer-system@1.0.0", "prompt:writer-user@1.0.0"]
  kind: 'system+user' | 'system-only' | 'user-only' | 'agent-reasoning';
  hash: string;                // sha256:... of composed body
  // observability: 'full' only
  systemPrompt?: string;
  userPrompt?: string;
  variableBindings?: Record<string, unknown>;
  // observability: 'hashed' or 'full'
  variableHashes?: Record<string, string>;  // name → sha256:...
  contentTrust?: 'trusted' | 'untrusted';
}

Under observability: "hashed" (default), only hash + variableHashes are populated — the bodies stay out of the event log. Under observability: "full", the composed bodies appear with secret redaction and trust-marker preservation.

Replay determinism

prompt.composed events participate in replay. Invariants:

  • hash MUST replay identically.
  • variableHashes[name] MUST replay identically.
  • refs MUST replay identically.
  • systemPrompt / userPrompt / variableBindings MAY be omitted on replay even when present in the original run (the host's observability setting may differ between original and replay); replay consumers MUST tolerate omission.

Divergence of hash MUST emit a replay.diverged event with divergencePoint: "prompt.composed" per replay.md.

Three-surface taxonomy (non-normative)

OpenWOP distinguishes three orthogonal observability surfaces for LLM-call inspection. Multi-agent debugging tools render all three to reconstruct what an agent saw + thought + emitted:

SurfaceWhat it capturesSource
prompt.composed.systemPrompt / userPromptThe body the host sent to the LLM, post-substitution + post-redactionThis document
AIEnvelope.payload.reasoningChain-of-thought the LLM emitted as part of structured outputRFC 0030 (parallel track, Draft)
agent.reasoning.delta + agent.reasonedThe LLM's interleaved thinking-tokens streamRFC 0024 (Accepted)

None of these replaces the others. A complete multi-agent visualization renders all three streams in temporal order so an operator can see what the host instructed, what the model thought through, and what the model returned as structured output.


Security invariants

The prompt.composed event MUST carry two SECURITY invariants once a host actually emits the event (per the staging precedent in RFC 0021):

  • prompt-composed-secret-redaction — Any variable whose source is secret MUST appear as [REDACTED:<secretId>] in systemPrompt, userPrompt, and variableBindings. Never as plaintext.
  • prompt-composed-trust-marker — When ANY contributing input was tagged meta.contentTrust: "untrusted" (per RFC 0020 §D), the composed bodies MUST wrap the untrusted segments in <UNTRUSTED>...</UNTRUSTED> markers AND the payload's contentTrust MUST be "untrusted".

Both invariants live in SECURITY/invariants.yaml once a reference host emits the event. Until then, the rows are RFC-tracked but not gate-enforced — matching the RFC 0021 envelope-shape staging precedent (the invariants land alongside the reference impl that emits the event, not at Draft merge).

The RFC 0028 Tier-2 endpoint surface carries two additional invariants covering the workspace-scoped authz contract:

  • prompt-mutation-workspace-membership-enforced — Hosts advertising capabilities.prompts.mutableLibrary: true MUST verify the authenticated principal's workspace membership before honoring any POST / PUT / DELETE to a workspace-scoped /v1/prompts* resource. workspaceId carried by the caller MUST NOT be trusted as authorization on its own. The canonical refusal envelope on 403 is error: "workspace_membership_required".
  • prompt-read-workspace-membership-enforced — Hosts advertising capabilities.prompts.supported: true MUST verify the authenticated principal's workspace membership before returning any workspace-scoped content via GET /v1/prompts?workspaceId=, GET /v1/prompts/{templateId} (when the host derives a workspace from the templateId), or POST /v1/prompts:render (when workspaceId is in the request body). A 200 response containing another workspace's templates is a cross-tenant data leak — the read-side invariant is symmetric to the write-side. Same canonical refusal envelope.

See §"Discovery & distribution" §"REST endpoints" §"Workspace membership on workspace-scoped reads and writes" below for the full normative text, including the application-tier-check-MUST-replicate-the-vendor's-RLS-rules guidance.


Discovery & distribution (RFC 0028)

Phase B of the prompt-library track adds two complementary surfaces:

REST endpoints

Six operations under /v1/prompts*, all gated on capabilities.prompts.endpointsSupported: true (NOT supported; see §"Capability advertisement" above for the two-axis split — supported gates node-execution PromptRef resolution, endpointsSupported gates this REST surface). The mutating three (POST / PUT / DELETE) are additionally gated on capabilities.prompts.mutableLibrary: true. Hosts that don't advertise the relevant capability return 501 capability_not_provided.

MethodPathOperationIdPurpose
GET/v1/promptslistPromptTemplatesPaginated list with ?kind, ?tag, ?modelClass, ?source filters + opaque cursor + limit.
POST/v1/promptscreatePromptTemplateCreate a user-source template (mutable libraries only). Returns 201 with a Location header.
GET/v1/prompts/{templateId}getPromptTemplateFetch a single template, optionally pinned via ?version. ETag + If-None-Match revalidation. ?libraryId disambiguates when packs collide.
PUT/v1/prompts/{templateId}updatePromptTemplateReplace a user-source template; submitted SemVer MUST be strictly greater than stored.
DELETE/v1/prompts/{templateId}deletePromptTemplateDelete a user-source template; 403 on host-built-in or pack-sourced.
POST/v1/prompts:renderrenderPromptTemplateRender with supplied bindings; returns composed body + sha256 hash + per-variable hashes. Does NOT dispatch an LLM call.

Deterministic-render invariant. The :render response's hash MUST equal the hash that a matching prompt.composed event would carry at dispatch time for the same (ref, variables, contentTrust) inputs. This is the same determinism contract prompt.composed participates in for replay (per §"Replay determinism" above) — the :render endpoint is the preview surface that lets clients validate hashes before dispatch.

Cache semantics. GET /v1/prompts/{templateId} responses SHOULD set ETag: "<sha256-of-body>" and Cache-Control: max-age=60. When the request pinned ?version, hosts SHOULD upgrade to Cache-Control: public, max-age=31536000, immutable (mirrors node-packs.md §"Immutable artifact" semantics).

Authorization. Mutating endpoints MUST require authentication per auth.md. Hosts SHOULD scope by writer role; the spec defers role-mapping to host policy.

Workspace membership on workspace-scoped reads and writes (normative, RFC 0048 §D). When a request to /v1/prompts* targets a workspace-scoped resource — either a mutating POST / PUT / DELETE whose body or URL carries a workspaceId, OR a read (GET /v1/prompts, GET /v1/prompts/{templateId}, POST /v1/prompts:render) whose query string or body carries an explicit workspaceId — hosts MUST verify that the authenticated principal is a member of the target workspace BEFORE honoring the request. A workspaceId supplied by the caller (in the request body, URL path, or query string) MUST NOT be trusted as authorization on its own — the host MUST resolve workspace membership from the authenticated identity (per auth.md §"Identity claims — tenant · workspace · principal" and RFC 0048 §D's cross-workspace isolation MUST-NOT), independent of any caller-supplied workspace identifier. Non-members MUST be rejected fail-closed before any persistence occurs (mutations) or any workspace-scoped content is returned (reads). The canonical refusal envelope is 403 { "error": "workspace_membership_required" } per rest-endpoints.md §"Common error codes"; hosts MAY refuse with other 4xx/5xx codes (401 if interpreting the failure as authentication-level; 404 to avoid existence disclosure) — the canonical envelope shape is pinned only when the host chooses 403.

Hosts that authenticate against an identity provider but persist via a database vendor's privileged admin client (Firebase Admin SDK, Supabase service-role key, raw Postgres with a server-side connection, Convex equivalent) MUST replicate the membership check at the application tier rather than relying solely on the database vendor's row-level security rules — privileged admin clients bypass those rules. The vendor's rules are a defense-in-depth layer below the application-tier membership check, not a substitute for it. This pattern is the failure mode that lets adopters convince themselves the check is redundant: a developer reading the route handler can see the Firestore rules and reason "the rules will catch a cross-tenant write," when in fact the Admin SDK code path bypasses them entirely. The application-tier check is mandatory.

Read paths are NOT exempt just because they don't write. A GET /v1/prompts?workspaceId=<not-mine> that returns another workspace's templates is a cross-tenant data leak with the same blast radius as a cross-tenant write — sensitive prompt text, model hints, and variable schemas can carry intellectual property, customer data, or operational secrets. The read-side authz check is symmetric to the write-side check; the same workspace_membership_required envelope applies. Read-only hosts that expose workspace-scoped reads are subject to this invariant even if they never advertise mutableLibrary: true.

Verified by the prompt-mutation-workspace-membership-enforced SECURITY invariant (write path; capability-gated on mutableLibrary: true) and the prompt-read-workspace-membership-enforced SECURITY invariant (read path; capability-gated on prompts.supported: true with workspaceId-acceptance auto-detected from the probe response).

Prompt-pack distribution

A third pack kind alongside node packs (RFC 0003) and workflow-chain packs (RFC 0013). Distinguished by kind: "prompt" in the manifest:

{
  "name": "vendor.acme.editorial-prompts",
  "version": "1.0.0",
  "kind": "prompt",
  "engines": { "openwop": ">=1.1.0 <2.0.0" },
  "prompts": [
    {
      "templateId": "writer-system",
      "version": "1.0.0",
      "kind": "system",
      "text": "You are a careful editorial writer.",
      "tags": ["editorial"]
    }
  ]
}

See schemas/prompt-pack-manifest.schema.json for the full shape.

Install-time validation. When a host installs a prompt pack:

1. Verify the Ed25519 signature per registry-operations.md §"Signature verification" — same flow as node and chain packs. 2. Verify SRI integrity per registry-operations.md §"Subresource Integrity" — unchanged. 3. Compile each prompts[].text against prompt-template.schema.json and assert variable-reference closure (every {{varName}} in text either appears in variables[] or matches a canonical context key). 4. Resolve every entry in the manifest's dependencies block; an unresolvable entry rejects the install with prompt_pack_dependency_unresolvable. 5. Reject install with prompt_template_invalid on any of the above.

Pack-kind discriminator invariant. A manifest with kind: "prompt" MUST NOT also carry nodes[] or chains[] arrays. Same posture as RFC 0013's "negative example."

Conflict resolution (cross-pack templateId collision). When two installed prompt packs ship the same templateId:

  • Both surface in GET /v1/prompts with distinct meta.source: "pack" + meta.packName + meta.packVersion discriminators.
  • A stringy PromptRef (prompt:writer-system@1.0.0) without libraryId is rejected with prompt_ref_ambiguous when more than one match exists.
  • Clients disambiguate by using the structured object form: { libraryId: "vendor.acme.editorial-prompts", templateId: "writer-system", version: "1.0.0" }.

Capability advertisement extensions

Phase B extends capabilities.prompts with three additional optional fields:

FieldSemantics
packsSupported: booleanHost installs kind: "prompt" packs and exposes their templates at GET /v1/prompts with meta.source: "pack". False or absent = no pack support.
mutableLibrary: booleanHost honors POST / PUT / DELETE /v1/prompts*. False or absent → 501 on those endpoints. Pack-sourced + host-built-in templates remain read-only regardless.
library.{id, renderEndpoint, maxRenderRequestBytes}Per-library configuration knobs. id enables structured-PromptRef libraryId lookup. renderEndpoint overrides the default /v1/prompts:render path. maxRenderRequestBytes caps :render request body size.

Provenance fields on PromptTemplate.meta

When a template is pack-sourced, the host MUST populate two additional meta fields:

  • meta.packName — matches the installed pack's name. Required when meta.source: "pack".
  • meta.packVersion — matches the installed pack's version. Required when meta.source: "pack".

A JSON-Schema if/then conditional in prompt-template.schema.json enforces this at install time.


Resolution chain (normative)

When a host advertises capabilities.prompts.supported: true and a workflow node carries one or more *PromptRef keys (per §"Composition + observability" above), the host MUST resolve each (nodeId, kind) pair to a single PromptRef (or null) by walking the four ordered layers below and selecting the first non-null result. Lower-numbered layers take precedence over higher-numbered layers. The traversal order is the same for all four kinds (system, user, few-shot, schema-hint); per-kind branches are noted inline.

For every (nodeId, kind) pair the host attempts to resolve, the host MUST emit one agent.promptResolved RunEventDoc (per schemas/run-event-payloads.schema.json agentPromptResolved) before the corresponding prompt.composed event (when emitted). The event's chain[] array carries one entry per layer attempted with applied: true on the winning layer.

Layer 1 — Node config (highest precedence)

The host first consults the executing WorkflowNode.config:

KindField consulted
systemWorkflowNode.config.systemPromptRef
userWorkflowNode.config.userPromptRef
few-shotWorkflowNode.config.fewShotPromptRefs[] (first non-empty entry; further entries surface as additionalPromptRefs for composition)
schema-hintWorkflowNode.config.schemaHintPromptRef

If the field is set, the resolved ref is that value and resolution halts. When the node carries an _inline_ string body in the corresponding sibling field (e.g., config.systemPrompt) AND a PromptRef, the ref wins and the warn-log rule from RFC 0027 §C applies (prompt_ref_supersedes_inline).

Layer 2 — Agent binding

Layer 2 is consulted only when capabilities.prompts.agentBindings: true is advertised AND the node carries config.agentId referencing a known AgentManifest. Resolution depends on the kind:

  • system kind:

- If AgentManifest.systemPrompt | systemPromptRef is set (the RFC 0003 intrinsic surface), the resolved ref is the agent's intrinsic prompt — a synthetic PromptRef projected from the manifest's tarball-relative path or inline body. Hosts MUST tag this chain entry as layer: "agent-intrinsic". - Else if AgentManifest.promptOverrides.system is set, that ref applies. Chain entry: layer: "agent-overrides". - Else if AgentManifest.promptLibraryRef is set, hosts MAY look up a same-kind default template from that library. Chain entry: layer: "agent-library-default". (The lookup convention — e.g., prompt:<libraryId>.default-<kind> — is host policy in v1.x.) - Else fall through to layer 3.

  • Other kinds (user, few-shot, schema-hint):

- If AgentManifest.promptOverrides[kind] is set, that ref applies. Chain entry: layer: "agent-overrides". - Else if AgentManifest.promptLibraryRef is set, host MAY apply library-default per above. - Else fall through to layer 3.

If config.agentId is unset, layer 2 is skipped entirely; resolution proceeds to layer 3. If config.agentId references an unknown agent, hosts MUST emit a log.appended warning with code: "agent_binding_unresolvable" and skip layer 2.

Layer 3 — Workflow defaults

If WorkflowDefinition.defaults.promptRefs[kind] is set, that ref applies. Chain entry: layer: "workflow-defaults". Else fall through to layer 4.

Layer 4 — Host built-ins (lowest precedence)

If the host's capabilities.prompts.defaults[kind] advertises a PromptRef, that ref applies. Chain entry: layer: "host-defaults". Hosts MAY ship per-kind defaults; the openwop spec ships none.

If all four layers yield null, the resolved ref for that (nodeId, kind) is null and the emitted agent.promptResolved.resolved is null. The node MAY still execute — for example, a core.ai.callPrompt node with userPrompt provided as a direct inline string in config doesn't need a user-kind ref. Whether a null resolution is fatal is per-node-type semantics, not protocol policy.

Run-configurable extension layer (optional, non-normative)

Hosts MAY honor RunOptions.configurable.promptOverrides as the highest-precedence layer — applied _before_ layer 1 in the traversal, taking precedence over node-config refs. When a host implements this extension, it MUST emit a chain entry with layer: "run-configurable" ahead of the node entry so cross-host debuggers render the additional step. This extension is non-normative in v1.x; a future RFC may promote it to a normative layer.

Replay determinism

agent.promptResolved events are durable and participate in replay. Invariants:

  • resolved MUST replay identically. Divergence MUST emit replay.diverged with divergencePoint: "agent.promptResolved".
  • chain[].applied MUST replay identically when resolved matches.
  • chain[].source SHOULD replay identically; if a host has rotated host-built-in defaults between original and replay, the host-defaults entry's source MAY differ. Replay tooling MUST tolerate this and surface the rotation explicitly rather than treating it as an integrity failure.

Orthogonality with model-capability gating (non-normative)

Per the envelope-track RFC 0031 (Draft), NodeModule.requiredModelCapabilities and NodeModule.fallbackModel answer a different question — _which model can dispatch this node?_ — and emit model.capability.substituted / model.capability.insufficient events. The two surfaces are orthogonal axes:

  • Prompt resolution (this section) answers "which PromptRef applies at (nodeId, kind)?" — does not influence model selection.
  • Model-capability gating (RFC 0031) answers "which model can dispatch this node?" — does not influence prompt selection.

A node MAY carry both surfaces independently. The agent.promptResolved event emitted by this section and the model.capability.* events emitted by RFC 0031 are distinct observability surfaces and MAY both fire for the same node execution. No precedence rule applies between them.


Open spec gaps

#GapOwner / RFC
P1Reference-host implementation of the four-layer resolution chain + agent.promptResolved emission in core.ai.callPromptAcceptance-gate item per RFC 0029 (wire shape landed in this document §"Resolution chain (normative)")
P2Reference-host emission of prompt.composed from core.ai.callPrompt in the workflow-engine sampleAcceptance-gate item per RFC 0027
P3First non-steward host advertises capabilities.prompts.supported: trueAcceptance-gate item per RFC 0027
P4Reference-host implementation of /v1/prompts* REST endpoints + prompt-pack install flowAcceptance-gate item per RFC 0028
P5Nested template includes ({{include:prompt:other@1.0.0}}) — deferred to a future RFC if demand emerges(open)
P6Canonical enumeration of context source variable names — currently non-normative recommendations(open)
P7Cross-validation of modelHints.envelopeType against capabilities.supportedEnvelopes and the Tier-1 portability subset (RFC 0030)RFC 0030 (Draft, parallel track)
P8Cross-pack dependencies semantics — extends: template inheritance, transitive closure resolutionRFC follow-up to 0028 (deferred)

Cross-reference

  • schemas/workflow-definition.schema.json (config description) + §"Resolution chain" Layer 1 above — the convention by which WorkflowNode.config carries PromptRef values.
  • capabilities.md — discovery handshake (this document extends the prompts block).
  • host-capabilities.md §host.aiEnvelope — the existing LLM-call surface that prompt resolution feeds.
  • mcp-integration.md §"Trust boundary" + RFC 0020 §D — meta.contentTrust propagation that prompt.composed.contentTrust mirrors.
  • replay.md — replay invariants this document extends with prompt.composed.
  • RFCS/0027-prompt-templates.md — Phase A wire-shape RFC (this document's source).
  • RFCS/0028-prompt-library-endpoints.md — Phase B registry + endpoint surface.
  • RFCS/0029-prompt-override-hierarchy.md — Phase C resolution chain + agent.promptResolved.
  • RFCS/0030-envelope-reasoning-and-tier-one-subset.md — parallel-track sibling for the LLM-emission side of the three-surface taxonomy.
  • SECURITY/threat-model-secret-leakage.md SR-1 — [REDACTED:<id>] marker discipline.
  • SECURITY/threat-model-prompt-injection.md<UNTRUSTED>...</UNTRUSTED> marker discipline.
  • External: MyndHyve PromptEntry / PromptLibrary / WorkflowPromptService.resolveForExecution() reference impl — closest single-host prior-art for the resolution-chain pattern RFC 0029 normates.