You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
As a workflow author, I want xmd workflow start to execute the exact Markdown file I supplied, including an untracked file or one outside a Git repository, so starting and resuming a workflow does not require Git and always uses one immutable definition.
xmd workflow start ./prepare-release.md
The command snapshots the file's current bytes before the run becomes durable. The first execution and every resume use that retained snapshot even if the original file is edited, moved or deleted.
Current gap
workflow start currently locates a repository, resolves HEAD, and reads the document with repositoryRoot(), revParse() and readGitObject(). Consequently:
a file outside Git cannot start;
an untracked file cannot start;
a modified tracked file executes its older committed bytes; and
Workflow's definition identity, source retrieval, run record and journal binding all depend on Git object fields.
That dependency also prevents #822 from moving repository collaboration into @executablemd/git: Workflow still needs Git to load its own definition, while the Git Plugin needs Workflow's durable execution boundaries.
Definition v2
New starts and new candidate definitions for forks use this closed descriptor:
interfaceSourceBundleWorkflowDefinitionV2{readonlyversion: 2;readonlykind: "source-bundle";readonlyhashAlgorithm: "sha256";readonlybundleHash: string;readonlyentrypoint: string;readonlysources: readonlySourceBundleEntryV2[];/** One exact canonical document target, without a leading `#`. */readonlytargetPath?: string;readonlycomponents?: readonlySourceBundleComponentV2[];}interfaceSourceBundleEntryV2{readonlypath: string;readonlysourceHash: string;readonlybyteLength: number;}interfaceSourceBundleComponentV2{readonlyname: string;readonlypath: string;}
The descriptor admits exactly those members. bundleHash and every sourceHash are 64 lowercase hexadecimal digits. byteLength is a non-negative safe integer. sources is non-empty and sorted by the UTF-8 byte order of path; every path appears once. entrypoint equals exactly one source path.
components, when present, is non-empty and sorted by the UTF-8 byte order of name; every name appears once and every component path names a retained source. Absence means the definition declares no workflow components, while an empty array is refused. Component names retain the existing XMD component-name grammar and existing exclusions for structural, engine-owned and host-reserved names.
A root without an explicit workflow.components declaration produces one source entry. Existing explicit workflow components remain a supported closed dependency: their Markdown bytes produce additional source entries and the components mapping on the same v2 descriptor. The sources array can therefore carry later dependency closure without another definition-version or identity migration; adding, removing or changing a retained dependency changes the bundle identity.
The ordinary definition serializer emits members in the order shown above, with targetPath and components omitted when absent. Object-member order is presentation, not identity: a parser admits those exact members in any order, while storage and artifacts may apply their own canonical JSON key ordering. Unknown members, duplicate or non-canonically ordered source/component array entries, an empty optional collection, or a hash or length outside its grammar is refused.
Logical paths
A logical path is a portable identity inside the source bundle, not a host filesystem path. It:
is a non-empty NFC-normalized Unicode string containing only Unicode scalar values;
uses / separators and never begins or ends with /;
contains no NUL, C0 control, DEL, backslash or # character;
contains no empty, . or .. segment; and
is compared case-sensitively by its UTF-8 bytes, without locale or filesystem normalization.
The entrypoint must end in .md. For the current one-file common path, the host derives it from the supplied file's final path segment after NFC normalization. The containing directory, absolute path, invocation working directory and platform separators never enter the descriptor. Existing declared component paths are resolved relative to the source file before the run exists, then represented by canonical logical paths in the same bundle.
Different host paths containing the same exact bytes under the same logical entrypoint describe the same definition. Changing the logical entrypoint is an identity change because source positions and future relative references use it.
Content addressing
All lengths are unsigned big-endian integers. u32(n) is four bytes, u64(n) is eight bytes, utf8(value) is the exact UTF-8 encoding, and field(value) is u32(utf8(value).length) followed by those UTF-8 bytes. A hexadecimal hash contributes its decoded 32 bytes.
SHA-256(
field("executablemd.workflow.bundle.v2") ||
field(entrypoint) ||
u32(source count) ||
for each canonically ordered source:
field(path) || decoded sourceHash || u64(byteLength) ||
u32(component count) ||
for each canonically ordered component:
field(name) || field(path)
)
The target is deliberately outside the source-bundle hash: selecting a section does not change the bytes in the bundle. targetPath remains part of the complete workflow-definition identity and therefore still distinguishes two runs over the same bundle.
No property values, run ID, host path, Git provenance, retrieval metadata, timestamp or storage encoding enters either hash. Source bytes are hashed and stored without newline, BOM, Unicode or other content normalization.
Exact source retention
Definition v2 storage retains source content as BLOB bytes, never as a database text value or re-encoded JSON string. A newly created Git v1 run keeps live-database PRAGMA user_version = 1; a newly created source-bundle v2 run uses PRAGMA user_version = 2. Initialization selects the schema from the already parsed candidate definition and never upgrades an existing database.
The schema declarations are two immutable, closed inventories. Version 1 is the current OBJECTS map byte-for-byte. Version 2 keeps every version-1 table, index and trigger byte-for-byte except workflow_run, replaces that table with the definition below, and adds exactly the two definition-source tables below. In particular, definition_retrieval remains an optional replaceable-metadata table and every Workspace, journal, session and lifecycle object is unchanged.
CREATETABLEworkflow_run (
id INTEGERPRIMARY KEYCHECK (id =1),
run_id TEXTNOT NULL,
definition TEXTNOT NULLCHECK (
json_valid(definition)
AND json_extract(definition, '$.version') =2AND json_extract(definition, '$.kind') ='source-bundle'
),
props TEXTNOT NULLCHECK (json_valid(props) AND json_type(props) ='object'),
status TEXTNOT NULLCHECK (
status IN ('running', 'suspended', 'interrupted', 'completed', 'failed', 'cancelled')
),
stop_reason_kind TEXTCHECK (
stop_reason_kind IS NULLOR stop_reason_kind IN ('host', 'journal')
),
stop_reason_code TEXT,
stop_reason_event_id TEXTREFERENCES journal_events (event_id),
created_at TEXTNOT NULL,
updated_at TEXTNOT NULL,
CHECK (
(stop_reason_kind IS NULLAND stop_reason_code IS NULLAND stop_reason_event_id IS NULL)
OR (stop_reason_kind ='host'AND stop_reason_code IS NOT NULLAND stop_reason_event_id IS NULL)
OR (stop_reason_kind ='journal'AND stop_reason_code IS NULLAND stop_reason_event_id IS NOT NULL)
)
) STRICT
CREATETABLEworkflow_definition_source (
pathTEXTPRIMARY KEYCHECK (length(path) >0),
source_hash TEXTNOT NULLREFERENCES workflow_definition_blob(source_hash) ON DELETE RESTRICT
) STRICT, WITHOUT ROWID
The descriptor parser, rather than SQLite collation, enforces the complete logical-path grammar and canonical order. The source table contains exactly one row per descriptor entry; multiple paths may reference one blob when their exact bytes are identical. Unreferenced blobs are corruption rather than tolerated garbage.
The stored manifest must equal the descriptor's complete sources array. Every referenced blob must exist, its byte length must equal both retained lengths, and recomputing its source hash must produce its key. Recomputing the bundle hash from the retained manifest and component mapping must produce bundleHash. An extra manifest row, an unreferenced blob, a missing entry, a length disagreement or a hash disagreement is corruption; no reader returns a partial bundle.
Before parsing as Markdown, the entrypoint and declared component sources must decode as well-formed UTF-8 under one strict decoder. Decoding performs no normalization. The BLOB remains authoritative and is what export and later resume preserve.
The public WorkflowRunStorage.create(CreateWorkflowRunRequest) initialization API remains v1-only, with its current exact { runId, definition: GitWorkflowDefinitionV1, base, props } request. Its parser refuses a v2 definition. lookup() recognizes both versions. This prevents a caller holding only a v2 descriptor from creating a database whose authoritative source bytes were never supplied.
V2 creation crosses only the existing trusted, non-contextual lifecycle transition. WorkflowRunCreation becomes this closed union:
sourceSnapshot is non-empty, admits no extra entry members, and has exactly the same paths in exactly the same order as definition.sources. Each bytes value is the candidate's exact byte sequence; its byte length and recomputed source hash must equal the corresponding descriptor entry. The transition takes an owned copy of every byte sequence before validation or persistence and retains only those copies, so later mutation of a caller-owned Uint8Array cannot change the run. A mismatch is an invalid creation and writes nothing.
The lifecycle begin, fork and private stageFork paths accept this union. Only their trusted transition implementation can turn the v2 member into storage: in its one initialization transaction it selects schema 2, writes the descriptor, manifest rows and de-duplicated BLOBs, establishes Workspace and lifecycle records, and begins the first execution. The lower-level public storage create() neither accepts nor partially stages v2.
The retained storage record is a closed discriminated union. The v1 member remains the current { runId, definition: GitWorkflowDefinitionV1, base, props, ... } shape. The v2 member is { runId, definition: SourceBundleWorkflowDefinitionV2, props, ... } and does not admit base or pinnedCommit; those are Git v1 fields, not empty or synthetic v2 values. All status, stop-reason and timestamp members represented by ... remain the current members in their current serialized order.
Recognition first validates the application ID, then dispatches only on user_version 1 or 2. Version 1 is accepted only when its schema inventory exactly equals the immutable v1 declaration. Version 2 is accepted only when its inventory exactly equals the immutable v2 declaration above. Version 0 with either declared inventory is corruption, any other version is unsupported, and a hybrid or extra object is corruption. Recognition never repairs, migrates or reinterprets a database. After structural recognition, a v2 reader parses the closed record and performs the full manifest/blob/hash verification before returning a run or attaching lifecycle state.
Version-1 live databases and run records keep their existing schema and exact meaning. Status and history expose the discriminated definition they retained; human output describes a v2 source bundle by its entrypoint and bundle hash instead of printing a fabricated Git base or commit.
Target semantics
The path passed to workflow start locates the input only. An optional authored selector is resolved against the exact candidate entrypoint bytes before storage is created. The descriptor retains only the one exact canonical document target produced by core, without #; a glob, alias or other request spelling is never identity.
Absent targetPath selects the whole entrypoint. Present targetPath must satisfy core's canonical target grammar and must resolve in the retained entrypoint. It participates in run-ID compatibility, the workflow journal binding, root-import admission and artifact verification. Resume uses the retained exact target and never resolves the caller's original selector again.
Start and persistence ordering
For start, the host performs these phases in order:
Read the supplied file once as bytes. Derive its logical entrypoint, validate the path and strict UTF-8, parse the document, resolve the exact target, validate the declared component closure and props, and construct and verify the canonical v2 descriptor. None of this creates run storage or executes authored content.
Acquire the executor lock for the chosen run ID. A compatible existing run is handled under the reuse rules below; its retained source is verified before another document-execution record is begun.
For a new run, one storage transaction retains the v2 run record, complete source manifest and exact blobs, initial Workspace state, first document-execution record and running lifecycle state. The transaction commits all of them or none of them.
Only after that commit may the CLI report workflow run:, import the root or execute authored content. The execution uses the committed snapshot, with its retained logical path and exact target.
Failure or cancellation in phases 1–3 leaves no recognized runnable run and no reported run ID. Repeating start with that ID can create it normally; an empty or private staging file is not a damaged run and is not discoverable by lookup or list. Once phase 3 commits, the run owns a complete definition snapshot. Interruption before the first authored effect is an ordinary interrupted execution that can resume from those retained bytes.
Fork uses the same v2 establishment for its candidate. Its already-required atomic admission retains the candidate descriptor and exact source store together with the inherited prefix, roots, lineage and first execution record. A failed compatibility preflight or failed admission leaves no discoverable destination run.
Run-ID reuse
A v2 start reusing an ID is compatible only when all of these agree:
definition version, kind and hashAlgorithm;
bundleHash, entrypoint, the complete canonical source manifest and component mapping;
exact presence and value of targetPath; and
normalized props.
The comparison checks the complete manifest even though the bundle hash commits to it; a hash is not used to excuse malformed or conflicting retained structure. The candidate's host path and optional provenance are ignored. Different bytes, logical paths, component mappings or target selection conflict as definition; different props conflict as props. A compatible reuse executes or replays the retained source, never the newly supplied file buffer.
A v1 run remains compatible only under its existing v1 comparison, including Git object format, object ID, repository-relative path, exact target, declared component records, base and props. V1 and v2 descriptors are never compatible with one another.
Durable and public WorkflowRun
The workflow journal record and the value returned by getWorkflowRun() are one closed union:
V1 preserves its exact three-member JSON representation. The ordinary v2 serializer emits runId, definitionVersion, bundleHash, then targetPath when present. Object-member order is not identity: parsing first recognizes the exact v1 member set or exact v2 member set in any order and then validates every value; it never admits another member or fills a synthetic base, pinnedCommit or target. A canonical-JSON container sorts these keys under its existing rule without changing the value.
The retained workflow effect description remains { type: "workflow_run", name: "workflow_run", base } for v1. Its v2 form is { type: "workflow_run", name: "workflow_run", definitionVersion: 2, bundleHash }. The journal record value is the complete union member above. getWorkflowRun() returns that frozen value, retainedWorkflowInstallation() accepts that union, and root document-execution records written by start or fork retain the matching union member. V2 admission cross-checks bundleHash and exact target presence/value against the complete storage descriptor before a document imports.
The existing public workflowInstallation({ base }) entrypoint remains a v1 Git convenience in this story, including its current Git lookup. #822 moves that adapter into the bundled Git Plugin and removes the final package-level Workflow-to-Git import. #443 changes the retained start/resume/fork/export path and the shared storage/journal contracts needed by that extraction; it does not claim that every exported Workflow module is already Git-free.
Legacy Git definitions
GitWorkflowDefinitionV1, its JSON shape, Git-object identity, run record, journal binding and compatible-reuse rules remain byte-for-byte and semantically unchanged. A v1 run is never rewritten as v2 merely because its source was retrieved successfully.
The retained workflow lifecycle parses and retains v1 identity but no longer invokes Git directly to obtain its source. A trusted host may supply this direct, non-contextual dependency:
The reader is captured by the workflow host before document code runs and is reachable through no Context, contextual API, component, Plugin installation result or authored value. It receives only the parsed v1 descriptor and its replaceable retrieval metadata. The Deno/compiled host initially supplies the existing Git-object reader through this seam; #822 moves that adapter into @executablemd/git without adding a Workflow-to-Git dependency.
Workflow validates the returned root identity, exact target, component set, paths, declared Git blob hashes and Markdown bytes against the v1 descriptor before lifecycle admission. A missing reader, unavailable checkout or object, malformed response, and response that disagrees with the descriptor are distinct failures. None substitutes HEAD, a working-tree file or an empty source, and none mutates the v1 run.
All source availability checks needed to start a v1 resume occur under the executor lock but before stale recovery, a new document-execution record, Workspace attachment, journal replay or root import. A failure therefore leaves the run's lifecycle and journal unchanged. A completed v1 replay, export or fork source uses the same host reader whenever its retained journal or artifact does not already carry an authenticated source closure.
Provenance
Git is optional provenance for v2, not definition identity or source retrieval. A host may retain a credential-free observation such as object format, commit and repository-relative path in the existing replaceable metadata boundary. It may be absent, replaced or become unreachable without changing compatibility or preventing resume.
The supplied absolute source path, checkout path and invocation working directory are never retained in the definition, run identity, journal binding or artifact. A provenance failure cannot fail a v2 start after the source bundle itself was established.
Failure contract
Invalid candidate: an unreadable input, invalid logical path, invalid UTF-8, malformed Markdown, unresolved target, invalid component declaration, invalid props, or a v2 lifecycle snapshot whose entries, lengths or hashes disagree with its descriptor refuses before run storage exists.
Incompatible reuse: a stored run under the requested ID has a different definition or props. The refusal names only the differing field, never source bytes, props or host paths.
Missing v2 source: the descriptor names a manifest entry or blob the live store does not contain. This is WorkflowDefinitionSourceMissingError, leaves the run unchanged and never consults the original file, provenance or legacy reader.
Corrupt v2 source: retained paths, lengths, hashes, bytes, component mappings or bundle hash disagree. This is WorkflowDefinitionCorruptError, returns no partial source and leaves the run unchanged.
Legacy reader unavailable: a v1 run needs source and its host installed no legacy reader. This is LegacyWorkflowSourceReaderUnavailableError and tells the operator to use a Git-capable XMD host; it creates no execution record.
Legacy retrieval unavailable: the installed reader cannot authenticate or read the retained commit and path. This is LegacyWorkflowSourceUnavailableError; it does not fall back to current HEAD or working-tree bytes.
Legacy response mismatch: the reader returns a source closure that does not describe the v1 definition. This is LegacyWorkflowSourceMismatchError; no returned bytes execute.
Persistence failure or cancellation: failure before the atomic creation commit publishes neither a runnable run nor workflow run:. Failure after the commit follows ordinary lifecycle settlement because the complete definition is already durable.
Stored-source errors do not quote source content, props, retrieval metadata or absolute paths. A malformed retained descriptor or database structure remains a storage-corruption refusal under the existing recognition rules rather than being reclassified as an unavailable external source.
Artifacts and portability
A v2 run remains exportable and inspectable without the original file or Git. It uses artifact format 2 while retaining the current physical SQLite container unchanged: the application ID, tables, indexes and PRAGMA user_version = 1 remain the exact container-schema-v1 declaration. Its header has artifact_version = 2 and container_version = 1; its canonical manifest is { "version": 2, "entries": [...] }; and artifact identity is computed with the domain prefix xmd-artifact\0v2\0. The manifest entry order, canonical-JSON rules and content-digest algorithm otherwise remain the format-1 rules.
Format 2 has this complete closed content-kind inventory:
It admits no format-1 Git definition-source kind. Each descriptor source contributes exactly two entries whose natural identity is the canonical JSON string value of its logical path:
definition-source-entry uses canonical-json encoding and contains exactly the members { "path": path, "sourceHash": sourceHash, "byteLength": byteLength }. Canonical JSON writes those keys in its existing lexicographic order (byteLength, path, sourceHash); readers require the exact member set but do not treat object-member order as identity.
definition-source-content uses bytes encoding and contains the source's exact BLOB bytes.
The workflow-run entry carries only the v2 definition and v2 WorkflowRun; it admits no base or pinnedCommit. Semantic verification requires exactly one entry/content pair per descriptor path and no undeclared pair, checks both identities against the path, checks the declared and manifest lengths, recomputes every source hash, and recomputes the bundle hash before returning status or history.
Artifact format 1, its xmd-artifact\0v1\0 identity domain and its Git definition closure remain byte-for-byte unchanged and readable. A format-1 artifact admits only the v1 workflow definition; a format-2 artifact admits only the v2 source-bundle definition. The writer selects format 1 for a v1 live run and format 2 for a v2 live run. The reader selects the exact closed inventory, manifest parser, identity domain and semantic verifier from the header version and never interprets one version's closure as the other.
Exporting a v1 live run obtains its closure through the host-supplied legacy reader. Exporting v2 reads only the retained source store. The format-2 closure is sufficient for the separately tracked artifact-backed fork to copy the descriptor and exact bytes into a destination run without making the artifact path retrieval state.
specs/xmd-artifact-spec.md records the unchanged physical container, both artifact versions, both closed inventories, their identity domains and the two definition-closure variants.
Acceptance
A Markdown file outside a Git repository starts and executes its exact current bytes.
An untracked or modified file inside a repository executes its working-tree bytes rather than the version in HEAD.
A root without declared workflow components produces the exact v2 descriptor above with one logical entrypoint and one source; existing explicit component bundles remain closed over their exact bytes through additional canonical entries.
Moving, editing or deleting the original file after start does not affect resume, completed replay, export or compatible reuse.
Source and bundle hashes use the exact domain-separated encodings specified above, and retained BLOB bytes round-trip without normalization.
The snapshot is durably complete before root import or authored execution. A failure or cancellation before the creation commit leaves no recognized run and permits a clean retry of the ID.
Whole-document and exact-target runs remain distinct; resume never re-resolves a selector.
Compatible reuse executes the retained source and compares the complete v2 definition plus normalized props. V1, v2, changed source, changed logical paths, changed components and changed targets are incompatible as specified.
Missing or corrupt v2 source refuses without fallback or partial content and without advancing lifecycle state.
Existing v1 live runs and artifacts retain their exact identity and remain readable, resumable and exportable through a host-supplied legacy source reader; Workflow itself performs no Git operation.
A missing, unavailable or disagreeing legacy reader fails categorically before lifecycle mutation and never substitutes another source.
Optional Git provenance and every absolute host path remain outside authoritative identity and are unnecessary for v2 resume.
V2 status, history, fork and artifact surfaces preserve their existing behavior without synthetic Git fields.
The CLI retained-definition path establishes, executes, resumes and exports v2 without a Git provider; v1 source retrieval crosses only the host-supplied legacy-reader seam. The existing public workflowInstallation({ base }) v1 convenience remains explicitly assigned to Make Git and GitHub available in XMD runs and workflows #822.
A v1 live database keeps exact schema version 1. A v2 live database has exact schema version 2, the declared closed inventory and no base column. Neither is migrated or recognized as the other.
Artifact format 1 remains exact for v1; format 2 uses the unchanged physical container, its own manifest and identity domain, the complete closed inventory above and only a v2 source bundle.
Journal serialization, getWorkflowRun(), retained installation and fork roots use the exact v1/v2 WorkflowRun union above. Ordinary serializers use the declared presentation order; canonical-JSON storage sorts object keys; readers accept either order but no different member set.
Public WorkflowRunStorage.create() remains v1-only and refuses a v2 definition. Trusted begin, fork and stageFork accept the exact WorkflowRunCreation union and atomically retain owned copies of every v2 source byte.
architecture.md, specs/workflow-spec.md, the affected workflow sections of specs/executable-mdx-spec.md, specs/workflow-workspace-spec.md and specs/xmd-artifact-spec.md describe the v1/v2 identity, source retention, lifecycle ordering, failures and portability boundary.
Lifecycle tests start from an outside-repository file, an untracked file and a modified tracked file; mutate or remove each source after creation; then resume and replay the retained bytes. Cancellation probes each phase before the atomic commit and proves the ID is absent and reusable. Another interrupts immediately after commit and proves the complete snapshot resumes.
Compatibility tests cover identical content reached through another host path, changed bytes, changed logical entrypoint, source order, component closure, target and props. Storage mutation tests remove and alter manifest rows and BLOBs and prove the exact missing/corrupt classifications with no lifecycle transition or fallback read.
Legacy fixtures created by the released v1 implementation resume, replay and export under a Git-capable host reader. Negative controls omit the reader, remove the retained Git object and return a mismatched closure; each leaves the run unchanged. An import-boundary test proves retained lifecycle and source-retrieval modules do not import Git or @executablemd/git; it permits only the existing workflowInstallation({ base }) v1 adapter that #822 removes.
Schema fixtures prove exact recognition of v1 and v2 inventories, refusal of version 0, unsupported versions, hybrids and extra objects, and absence of migration. Public-contract tests cover exact JSON member sets, ordinary serializer presentation and canonical-JSON ordering for both WorkflowRun variants through journal replay, getWorkflowRun(), retained installation and fork roots; reordered object members parse to the same value while reordered source/component arrays are refused.
Creation-boundary tests prove public storage creation still admits only v1, while trusted v2 begin, fork and private staging require a complete canonical sourceSnapshot, copy caller-owned buffers, reject missing, extra, reordered or mismatched entries before mutation, and commit the descriptor and exact BLOBs with the first execution.
Artifact export and inspection round trips cover v1 and v2 independently, including header/manifest version selection, both identity domains, the closed format-2 inventory and v2 exact-byte preservation after the original source disappears.
Make Git and GitHub available in XMD runs and workflows #822 may be planned and implemented only after this story is delivered. It consumes the merged v2 storage/journal/source-reader boundary, moves workflowInstallation({ base }) into the bundled Git Plugin and then removes the final package-level Workflow-to-Git dependency.
Out of scope
Automatic discovery of undeclared dependency closure beyond the existing explicit workflow.components bundle.
Treating provenance as identity or using it to reconstruct missing v2 source.
In-place conversion of a v1 live run or artifact to v2.
Delivering the artifact-backed history fork; this story only keeps its authenticated source input representable.
Standard-input workflow definitions, URL definitions or package-specifier definitions.
Story
As a workflow author, I want
xmd workflow startto execute the exact Markdown file I supplied, including an untracked file or one outside a Git repository, so starting and resuming a workflow does not require Git and always uses one immutable definition.xmd workflow start ./prepare-release.mdThe command snapshots the file's current bytes before the run becomes durable. The first execution and every resume use that retained snapshot even if the original file is edited, moved or deleted.
Current gap
workflow startcurrently locates a repository, resolvesHEAD, and reads the document withrepositoryRoot(),revParse()andreadGitObject(). Consequently:That dependency also prevents #822 from moving repository collaboration into
@executablemd/git: Workflow still needs Git to load its own definition, while the Git Plugin needs Workflow's durable execution boundaries.Definition v2
New starts and new candidate definitions for forks use this closed descriptor:
The descriptor admits exactly those members.
bundleHashand everysourceHashare 64 lowercase hexadecimal digits.byteLengthis a non-negative safe integer.sourcesis non-empty and sorted by the UTF-8 byte order ofpath; every path appears once.entrypointequals exactly one source path.components, when present, is non-empty and sorted by the UTF-8 byte order ofname; every name appears once and every component path names a retained source. Absence means the definition declares no workflow components, while an empty array is refused. Component names retain the existing XMD component-name grammar and existing exclusions for structural, engine-owned and host-reserved names.A root without an explicit
workflow.componentsdeclaration produces one source entry. Existing explicit workflow components remain a supported closed dependency: their Markdown bytes produce additional source entries and thecomponentsmapping on the same v2 descriptor. Thesourcesarray can therefore carry later dependency closure without another definition-version or identity migration; adding, removing or changing a retained dependency changes the bundle identity.The ordinary definition serializer emits members in the order shown above, with
targetPathandcomponentsomitted when absent. Object-member order is presentation, not identity: a parser admits those exact members in any order, while storage and artifacts may apply their own canonical JSON key ordering. Unknown members, duplicate or non-canonically ordered source/component array entries, an empty optional collection, or a hash or length outside its grammar is refused.Logical paths
A logical path is a portable identity inside the source bundle, not a host filesystem path. It:
/separators and never begins or ends with/;#character;.or..segment; andThe entrypoint must end in
.md. For the current one-file common path, the host derives it from the supplied file's final path segment after NFC normalization. The containing directory, absolute path, invocation working directory and platform separators never enter the descriptor. Existing declared component paths are resolved relative to the source file before the run exists, then represented by canonical logical paths in the same bundle.Different host paths containing the same exact bytes under the same logical entrypoint describe the same definition. Changing the logical entrypoint is an identity change because source positions and future relative references use it.
Content addressing
All lengths are unsigned big-endian integers.
u32(n)is four bytes,u64(n)is eight bytes,utf8(value)is the exact UTF-8 encoding, andfield(value)isu32(utf8(value).length)followed by those UTF-8 bytes. A hexadecimal hash contributes its decoded 32 bytes.Each source hash is:
The bundle hash is:
The target is deliberately outside the source-bundle hash: selecting a section does not change the bytes in the bundle.
targetPathremains part of the complete workflow-definition identity and therefore still distinguishes two runs over the same bundle.No property values, run ID, host path, Git provenance, retrieval metadata, timestamp or storage encoding enters either hash. Source bytes are hashed and stored without newline, BOM, Unicode or other content normalization.
Exact source retention
Definition v2 storage retains source content as BLOB bytes, never as a database text value or re-encoded JSON string. A newly created Git v1 run keeps live-database
PRAGMA user_version = 1; a newly created source-bundle v2 run usesPRAGMA user_version = 2. Initialization selects the schema from the already parsed candidate definition and never upgrades an existing database.The schema declarations are two immutable, closed inventories. Version 1 is the current
OBJECTSmap byte-for-byte. Version 2 keeps every version-1 table, index and trigger byte-for-byte exceptworkflow_run, replaces that table with the definition below, and adds exactly the two definition-source tables below. In particular,definition_retrievalremains an optional replaceable-metadata table and every Workspace, journal, session and lifecycle object is unchanged.The descriptor parser, rather than SQLite collation, enforces the complete logical-path grammar and canonical order. The source table contains exactly one row per descriptor entry; multiple paths may reference one blob when their exact bytes are identical. Unreferenced blobs are corruption rather than tolerated garbage.
The stored manifest must equal the descriptor's complete
sourcesarray. Every referenced blob must exist, its byte length must equal both retained lengths, and recomputing its source hash must produce its key. Recomputing the bundle hash from the retained manifest and component mapping must producebundleHash. An extra manifest row, an unreferenced blob, a missing entry, a length disagreement or a hash disagreement is corruption; no reader returns a partial bundle.Before parsing as Markdown, the entrypoint and declared component sources must decode as well-formed UTF-8 under one strict decoder. Decoding performs no normalization. The BLOB remains authoritative and is what export and later resume preserve.
The public
WorkflowRunStorage.create(CreateWorkflowRunRequest)initialization API remains v1-only, with its current exact{ runId, definition: GitWorkflowDefinitionV1, base, props }request. Its parser refuses a v2 definition.lookup()recognizes both versions. This prevents a caller holding only a v2 descriptor from creating a database whose authoritative source bytes were never supplied.V2 creation crosses only the existing trusted, non-contextual lifecycle transition.
WorkflowRunCreationbecomes this closed union:sourceSnapshotis non-empty, admits no extra entry members, and has exactly the same paths in exactly the same order asdefinition.sources. Eachbytesvalue is the candidate's exact byte sequence; its byte length and recomputed source hash must equal the corresponding descriptor entry. The transition takes an owned copy of every byte sequence before validation or persistence and retains only those copies, so later mutation of a caller-ownedUint8Arraycannot change the run. A mismatch is an invalid creation and writes nothing.The lifecycle
begin,forkand privatestageForkpaths accept this union. Only their trusted transition implementation can turn the v2 member into storage: in its one initialization transaction it selects schema 2, writes the descriptor, manifest rows and de-duplicated BLOBs, establishes Workspace and lifecycle records, and begins the first execution. The lower-level public storagecreate()neither accepts nor partially stages v2.The retained storage record is a closed discriminated union. The v1 member remains the current
{ runId, definition: GitWorkflowDefinitionV1, base, props, ... }shape. The v2 member is{ runId, definition: SourceBundleWorkflowDefinitionV2, props, ... }and does not admitbaseorpinnedCommit; those are Git v1 fields, not empty or synthetic v2 values. All status, stop-reason and timestamp members represented by...remain the current members in their current serialized order.Recognition first validates the application ID, then dispatches only on
user_version1 or 2. Version 1 is accepted only when its schema inventory exactly equals the immutable v1 declaration. Version 2 is accepted only when its inventory exactly equals the immutable v2 declaration above. Version 0 with either declared inventory is corruption, any other version is unsupported, and a hybrid or extra object is corruption. Recognition never repairs, migrates or reinterprets a database. After structural recognition, a v2 reader parses the closed record and performs the full manifest/blob/hash verification before returning a run or attaching lifecycle state.Version-1 live databases and run records keep their existing schema and exact meaning. Status and history expose the discriminated definition they retained; human output describes a v2 source bundle by its entrypoint and bundle hash instead of printing a fabricated Git base or commit.
Target semantics
The path passed to
workflow startlocates the input only. An optional authored selector is resolved against the exact candidate entrypoint bytes before storage is created. The descriptor retains only the one exact canonical document target produced by core, without#; a glob, alias or other request spelling is never identity.Absent
targetPathselects the whole entrypoint. PresenttargetPathmust satisfy core's canonical target grammar and must resolve in the retained entrypoint. It participates in run-ID compatibility, the workflow journal binding, root-import admission and artifact verification. Resume uses the retained exact target and never resolves the caller's original selector again.Start and persistence ordering
For
start, the host performs these phases in order:runninglifecycle state. The transaction commits all of them or none of them.workflow run:, import the root or execute authored content. The execution uses the committed snapshot, with its retained logical path and exact target.Failure or cancellation in phases 1–3 leaves no recognized runnable run and no reported run ID. Repeating
startwith that ID can create it normally; an empty or private staging file is not a damaged run and is not discoverable by lookup or list. Once phase 3 commits, the run owns a complete definition snapshot. Interruption before the first authored effect is an ordinary interrupted execution that can resume from those retained bytes.Fork uses the same v2 establishment for its candidate. Its already-required atomic admission retains the candidate descriptor and exact source store together with the inherited prefix, roots, lineage and first execution record. A failed compatibility preflight or failed admission leaves no discoverable destination run.
Run-ID reuse
A v2 start reusing an ID is compatible only when all of these agree:
version,kindandhashAlgorithm;bundleHash,entrypoint, the complete canonical source manifest and component mapping;targetPath; andThe comparison checks the complete manifest even though the bundle hash commits to it; a hash is not used to excuse malformed or conflicting retained structure. The candidate's host path and optional provenance are ignored. Different bytes, logical paths, component mappings or target selection conflict as
definition; different props conflict asprops. A compatible reuse executes or replays the retained source, never the newly supplied file buffer.A v1 run remains compatible only under its existing v1 comparison, including Git object format, object ID, repository-relative path, exact target, declared component records,
baseand props. V1 and v2 descriptors are never compatible with one another.Durable and public WorkflowRun
The workflow journal record and the value returned by
getWorkflowRun()are one closed union:V1 preserves its exact three-member JSON representation. The ordinary v2 serializer emits
runId,definitionVersion,bundleHash, thentargetPathwhen present. Object-member order is not identity: parsing first recognizes the exact v1 member set or exact v2 member set in any order and then validates every value; it never admits another member or fills a syntheticbase,pinnedCommitor target. A canonical-JSON container sorts these keys under its existing rule without changing the value.The retained workflow effect description remains
{ type: "workflow_run", name: "workflow_run", base }for v1. Its v2 form is{ type: "workflow_run", name: "workflow_run", definitionVersion: 2, bundleHash }. The journal record value is the complete union member above.getWorkflowRun()returns that frozen value,retainedWorkflowInstallation()accepts that union, and root document-execution records written by start or fork retain the matching union member. V2 admission cross-checksbundleHashand exact target presence/value against the complete storage descriptor before a document imports.The existing public
workflowInstallation({ base })entrypoint remains a v1 Git convenience in this story, including its current Git lookup. #822 moves that adapter into the bundled Git Plugin and removes the final package-level Workflow-to-Git import. #443 changes the retained start/resume/fork/export path and the shared storage/journal contracts needed by that extraction; it does not claim that every exported Workflow module is already Git-free.Legacy Git definitions
GitWorkflowDefinitionV1, its JSON shape, Git-object identity, run record, journal binding and compatible-reuse rules remain byte-for-byte and semantically unchanged. A v1 run is never rewritten as v2 merely because its source was retrieved successfully.The retained workflow lifecycle parses and retains v1 identity but no longer invokes Git directly to obtain its source. A trusted host may supply this direct, non-contextual dependency:
The reader is captured by the workflow host before document code runs and is reachable through no Context, contextual API, component, Plugin installation result or authored value. It receives only the parsed v1 descriptor and its replaceable retrieval metadata. The Deno/compiled host initially supplies the existing Git-object reader through this seam; #822 moves that adapter into
@executablemd/gitwithout adding a Workflow-to-Git dependency.Workflow validates the returned root identity, exact target, component set, paths, declared Git blob hashes and Markdown bytes against the v1 descriptor before lifecycle admission. A missing reader, unavailable checkout or object, malformed response, and response that disagrees with the descriptor are distinct failures. None substitutes
HEAD, a working-tree file or an empty source, and none mutates the v1 run.All source availability checks needed to start a v1 resume occur under the executor lock but before stale recovery, a new document-execution record, Workspace attachment, journal replay or root import. A failure therefore leaves the run's lifecycle and journal unchanged. A completed v1 replay, export or fork source uses the same host reader whenever its retained journal or artifact does not already carry an authenticated source closure.
Provenance
Git is optional provenance for v2, not definition identity or source retrieval. A host may retain a credential-free observation such as object format, commit and repository-relative path in the existing replaceable metadata boundary. It may be absent, replaced or become unreachable without changing compatibility or preventing resume.
The supplied absolute source path, checkout path and invocation working directory are never retained in the definition, run identity, journal binding or artifact. A provenance failure cannot fail a v2 start after the source bundle itself was established.
Failure contract
WorkflowDefinitionSourceMissingError, leaves the run unchanged and never consults the original file, provenance or legacy reader.WorkflowDefinitionCorruptError, returns no partial source and leaves the run unchanged.LegacyWorkflowSourceReaderUnavailableErrorand tells the operator to use a Git-capable XMD host; it creates no execution record.LegacyWorkflowSourceUnavailableError; it does not fall back to currentHEADor working-tree bytes.LegacyWorkflowSourceMismatchError; no returned bytes execute.workflow run:. Failure after the commit follows ordinary lifecycle settlement because the complete definition is already durable.Stored-source errors do not quote source content, props, retrieval metadata or absolute paths. A malformed retained descriptor or database structure remains a storage-corruption refusal under the existing recognition rules rather than being reclassified as an unavailable external source.
Artifacts and portability
A v2 run remains exportable and inspectable without the original file or Git. It uses artifact format 2 while retaining the current physical SQLite container unchanged: the application ID, tables, indexes and
PRAGMA user_version = 1remain the exact container-schema-v1 declaration. Its header hasartifact_version = 2andcontainer_version = 1; its canonical manifest is{ "version": 2, "entries": [...] }; and artifact identity is computed with the domain prefixxmd-artifact\0v2\0. The manifest entry order, canonical-JSON rules and content-digest algorithm otherwise remain the format-1 rules.Format 2 has this complete closed content-kind inventory:
It admits no format-1 Git definition-source kind. Each descriptor source contributes exactly two entries whose natural identity is the canonical JSON string value of its logical path:
definition-source-entryusescanonical-jsonencoding and contains exactly the members{ "path": path, "sourceHash": sourceHash, "byteLength": byteLength }. Canonical JSON writes those keys in its existing lexicographic order (byteLength,path,sourceHash); readers require the exact member set but do not treat object-member order as identity.definition-source-contentusesbytesencoding and contains the source's exact BLOB bytes.The
workflow-runentry carries only the v2 definition and v2WorkflowRun; it admits nobaseorpinnedCommit. Semantic verification requires exactly one entry/content pair per descriptor path and no undeclared pair, checks both identities against the path, checks the declared and manifest lengths, recomputes every source hash, and recomputes the bundle hash before returning status or history.Artifact format 1, its
xmd-artifact\0v1\0identity domain and its Git definition closure remain byte-for-byte unchanged and readable. A format-1 artifact admits only the v1 workflow definition; a format-2 artifact admits only the v2 source-bundle definition. The writer selects format 1 for a v1 live run and format 2 for a v2 live run. The reader selects the exact closed inventory, manifest parser, identity domain and semantic verifier from the header version and never interprets one version's closure as the other.Exporting a v1 live run obtains its closure through the host-supplied legacy reader. Exporting v2 reads only the retained source store. The format-2 closure is sufficient for the separately tracked artifact-backed fork to copy the descriptor and exact bytes into a destination run without making the artifact path retrieval state.
specs/xmd-artifact-spec.mdrecords the unchanged physical container, both artifact versions, both closed inventories, their identity domains and the two definition-closure variants.Acceptance
HEAD.workflowInstallation({ base })v1 convenience remains explicitly assigned to Make Git and GitHub available in XMD runs and workflows #822.basecolumn. Neither is migrated or recognized as the other.getWorkflowRun(), retained installation and fork roots use the exact v1/v2WorkflowRununion above. Ordinary serializers use the declared presentation order; canonical-JSON storage sorts object keys; readers accept either order but no different member set.WorkflowRunStorage.create()remains v1-only and refuses a v2 definition. Trustedbegin,forkandstageForkaccept the exactWorkflowRunCreationunion and atomically retain owned copies of every v2 source byte.architecture.md,specs/workflow-spec.md, the affected workflow sections ofspecs/executable-mdx-spec.md,specs/workflow-workspace-spec.mdandspecs/xmd-artifact-spec.mddescribe the v1/v2 identity, source retention, lifecycle ordering, failures and portability boundary.Evidence
Focused definition tests cover the closed v2 schema, logical-path grammar, canonical UTF-8 ordering, component mapping, target presence, domain-separated hash vectors, byte lengths and malformed variants.
Lifecycle tests start from an outside-repository file, an untracked file and a modified tracked file; mutate or remove each source after creation; then resume and replay the retained bytes. Cancellation probes each phase before the atomic commit and proves the ID is absent and reusable. Another interrupts immediately after commit and proves the complete snapshot resumes.
Compatibility tests cover identical content reached through another host path, changed bytes, changed logical entrypoint, source order, component closure, target and props. Storage mutation tests remove and alter manifest rows and BLOBs and prove the exact missing/corrupt classifications with no lifecycle transition or fallback read.
Legacy fixtures created by the released v1 implementation resume, replay and export under a Git-capable host reader. Negative controls omit the reader, remove the retained Git object and return a mismatched closure; each leaves the run unchanged. An import-boundary test proves retained lifecycle and source-retrieval modules do not import Git or
@executablemd/git; it permits only the existingworkflowInstallation({ base })v1 adapter that #822 removes.Schema fixtures prove exact recognition of v1 and v2 inventories, refusal of version 0, unsupported versions, hybrids and extra objects, and absence of migration. Public-contract tests cover exact JSON member sets, ordinary serializer presentation and canonical-JSON ordering for both
WorkflowRunvariants through journal replay,getWorkflowRun(), retained installation and fork roots; reordered object members parse to the same value while reordered source/component arrays are refused.Creation-boundary tests prove public storage creation still admits only v1, while trusted v2 begin, fork and private staging require a complete canonical
sourceSnapshot, copy caller-owned buffers, reject missing, extra, reordered or mismatched entries before mutation, and commit the descriptor and exact BLOBs with the first execution.Artifact export and inspection round trips cover v1 and v2 independently, including header/manifest version selection, both identity domains, the closed format-2 inventory and v2 exact-byte preservation after the original source disappears.
Dependencies and related work
workflowInstallation({ base })into the bundled Git Plugin and then removes the final package-level Workflow-to-Git dependency.Out of scope
workflow.componentsbundle.xmd runsource behavior.workflowInstallation({ base })convenience; Make Git and GitHub available in XMD runs and workflows #822 owns that extraction.