JSON Schema (v1)
The canonical JSON Schema (Draft 2020-12) for projectfile v1. Editors with JSON Schema support pick up validation automatically when a document references the canonical URL via the #:schema magic comment (TOML) or the $schema: key (YAML/JSON).
Download
Section titled “Download”Every schema is served verbatim at its canonical URL — the address its own $id declares. Follow a card to read or download the raw document.
projectfile v1Schema for the projectfile specification, version 1. See https://projectfile.org/spec/v1. Applies to the in-memory tree resulting from parsing a projectfile.toml, projectfile.yaml, or projectfile.json file. The schema enforces the structural contract (required fields, types, identifier shapes). Semantic SHOULDs and recommended vocabularies (technology tags, OS/arch values, ) are intentionally NOT enforced — see spec §3.4 and §3.7 (peaceful cohabitation). — Served at /schema/v1.json
org.projectfile.artifacts extension (v1)Standalone contract for the value at the org.projectfile.artifacts subtree — a map of NAMED artifacts a project PRODUCES: the container images it publishes, the packages it ships to a registry, the executables it installs, the services it exposes, the sites it deploys. One neutral home for the question "what does this project give me", answering both a CI tool that needs pointing at an output and a documentation generator that needs to say how to install and call it. Retires the stringly-typed space-separated `M6E_ARTIFACTS` list (which could not say WHICH artifact a given tool wanted) with named keys, so a binary analyzer (gsa) references one artifact and a site auditor (lighthouse) another. Each entry carries a `kind` plus the address fields that kind is addressed by. Referenced from a string anywhere in the document via the §3.8 interpolation sigil: by name for one artifact (`${org.projectfile.artifacts.<name>.path}`), or by the map selector for every artifact of a kind (`${org.projectfile.artifacts{kind=image}.ref}`), which fans a repeating output out to one entry per match. The selector takes several predicates, so an artifact addressable at more than one deployment carries an `environment` and is picked with `{kind=website,environment=ci}`. — Served at /schema/org.projectfile.artifacts.v1.json
org.projectfile.build extension (v1)Standalone contract for the value at the org.projectfile.build subtree of a projectfile document — the registry hosts and BUILD ARGUMENTS a project's container build depends on, declared ONCE and lowered to BOTH the local make plane (m6e) and a forge CI plane (the pf-ci). It carries only what `docker build` consumes: registry-host building blocks (also used at `docker login`/push) and a FLAT map of build-args, where a `FROM` image ref is just one such arg with its registry + tag interpolated in place (`${B19_DOCKER_REGISTRY}/b19/ubuntu/${B19_UBUNTU_SERIES}:${M6E_BASE_IMAGE_DEFAULT_VERSION}`). Images a TOOL RUNS IN (e.g. the container `go vet` executes inside) are NOT a build concern — they live under `org.projectfile.ci.images`, referenced by `org.projectfile.ci.tools.<t>.image`. Every key is a VARIABLE NAME and every value is an expression over OTHER such variables (`${NAME}`); no key names a registry host, a forge, or a CI engine — a generator substitutes its own spelling (m6e `$(NAME)` + `export NAME ?= …`; GitHub/Forgejo `${{ vars.NAME }}`). — Served at /schema/org.projectfile.build.v1.json
org.projectfile.ci extension (v1)Standalone contract for the value at the org.projectfile.ci subtree of a projectfile document — the vendor-AGNOSTIC CI intent expressed as a directed acyclic graph (DAG). The main projectfile schema (v1.json) treats this subtree as opaque (§3.7); this fragment is the cross-vendor interop contract a resolver validates the subtree against. The graph is built from free-named NODES (states/milestones — e.g. `code-is-valid`, `html-built`, `image-scanned`). Each node declares `needs` — a unified key that carries both DAG edges to other nodes and leaf tool invocations. Every entry in `needs` is a Make target; the reader does not distinguish node edges from tool calls — Make does. A node name is an OPAQUE label carrying no semantics a consumer must interpret; ALL realization lives in the tools, whose names are the only vendor-resolvable references. This carries WHICH tools run and in WHAT order, never HOW a tool runs (the runner/image mechanism lives in org.{vendor}.ci). The one parallelism concern that IS agnostic is the shape of the matrix product itself — its axes and, per cell node, an OPTIONAL `max-parallel` cap on how many of that node's cells materialise at once; the runner mechanism realising each cell still lives in the vendor overlay. An OPTIONAL `matrix` declares a cartesian product of axes; nodes flagged `matrix: true` are CELLS run once per product cell, a non-flagged node downstream of a cell is a JOIN fan-in run once after every cell, and the rest are SOURCE run once before — the vendor-neutral form of GitHub/Tekton/Forgejo `strategy.matrix`. Cooperating consumers lower the same graph natively: m6e -> make (node=target, needs=order-only prerequisites, cell=per-value sub-make), t4n -> Tekton (node=Task, needs=runAfter, cell=Task matrix), and GitHub/Forgejo workflow generators (node=job, needs=needs, cell=strategy.matrix). — Served at /schema/org.projectfile.ci.v1.json
org.projectfile.events extension (v1)Standalone contract for the value at the org.projectfile.events subtree — a project's OUTBOUND lifecycle-event surface, the dual of org.projectfile.ci's inbound triggers. Declared ONCE and lowered to BOTH the local make plane (m6e → a desktop notification) and a forge CI plane (the pf-ci → an outbound webhook step). Both planes emit the SAME neutral JSON envelope ({event, project, goal, status, url, ts, payload}); that envelope is the contract, so the transport is pure configuration — a future Apprise / Node-RED / Kafka sink is a different URL, not new code. Phase 1 is intentionally minimal (convention over configuration): the MERE PRESENCE of this subtree opts a project into lifecycle emission, with two built-in events (goal.succeeded, goal.failed) fanned to two built-in sinks (a desktop notification locally, a webhook in CI). The webhook sink is keyed off ONE forge variable NAME; its URL VALUE lives in the forge (a repo/org variable or secret), NEVER in the projectfile — so the secret is never committed and never rendered into a workflow, and an opted-in repo with no variable configured emits nothing (secure default). The richer sinks{} / on{} subscription surface (named sinks, per-event routing, custom event names) is RESERVED for a later revision; a phase-1 consumer treats an absent block as 'no emission' and a present block as 'the built-in defaults'. — Served at /schema/org.projectfile.events.v1.json
org.projectfile.image extension (v1)Standalone contract for the value at the org.projectfile.image subtree: the image a project publishes, declared as PARTS and TEMPLATES. Every name the image carries — its repository path, the tags each build publishes, and how a build variant enters a tag — is a template the document declares; the specification fixes no naming convention. The mechanism is defined in conformance/image/README.md. — Served at /schema/org.projectfile.image.v1.json
projectfile v1 includes fragmentSchema for a document that is only ever included (spec §4.9): the field shapes of v1.json, with no discriminator and no required fields. Validate the merged document against v1.json. — Served at /schema/v1-fragment.json
Core document schema
Section titled “Core document schema”{ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://projectfile.org/schema/v1.json", "title": "projectfile v1", "description": "Schema for the projectfile specification, version 1. See https://projectfile.org/spec/v1. Applies to the in-memory tree resulting from parsing a projectfile.toml, projectfile.yaml, or projectfile.json file. The schema enforces the structural contract (required fields, types, identifier shapes). Semantic SHOULDs and recommended vocabularies (technology tags, OS/arch values, ) are intentionally NOT enforced — see spec §3.4 and §3.7 (peaceful cohabitation).",
"type": "object", "$ref": "#/$defs/fragment", "properties": { "repositories": { "items": { "required": ["url"] } } },
"if": { "required": ["includes"] }, "else": { "description": "A document without includes carries its own identity. One with includes MAY take identity from them; validators check the merged document (spec §4.9).", "required": ["identity"], "properties": { "identity": { "required": ["namespace", "name"] } } },
"anyOf": [ { "description": "YAML/JSON discriminator: $schema must be present and equal the v1 URL (spec §3.4).", "required": ["$schema"], "properties": { "$schema": { "const": "https://projectfile.org/schema/v1.json" } } }, { "description": "TOML discriminator: spec_version must be present and equal '1' (spec §3.4). The schema cannot see the encoding, so validators enforce this branch for .toml files by extension; the #:schema magic comment is an editor hint and does not survive parse.", "required": ["spec_version"] } ],
"$defs": { "fragment": { "type": "object", "description": "Field shapes shared by a document and an includes fragment, with nothing required (spec §4.9).", "properties": { "$schema": { "enum": ["https://projectfile.org/schema/v1.json", "https://projectfile.org/schema/v1-fragment.json"], "description": "Discriminator for YAML/JSON encodings; pins the specification version. A fragment MAY name the fragment schema instead (spec §4.9)." }, "spec_version": { "const": "1", "description": "Discriminator for TOML encoding; pins the specification version." }, "kind": { "type": "string", "default": "SoftwareSourceCode", "description": "Schema.org type. Recommended: SoftwareSourceCode, SoftwareApplication, WebApplication.", "examples": ["SoftwareSourceCode", "SoftwareApplication", "WebApplication"] }, "identity": { "$ref": "#/$defs/identity" }, "repositories": { "$ref": "#/$defs/repositories" }, "license": { "$ref": "#/$defs/license" }, "copyright": { "$ref": "#/$defs/copyright" }, "upstream": { "$ref": "#/$defs/upstream" }, "people": { "type": "array", "items": { "$ref": "#/$defs/person" } }, "organizations": { "type": "array", "items": { "$ref": "#/$defs/organization" } }, "keywords": { "type": "array", "uniqueItems": true, "items": { "$ref": "#/$defs/keyword" } }, "technologies": { "type": "array", "uniqueItems": true, "items": { "$ref": "#/$defs/technology" } }, "requirements": { "$ref": "#/$defs/requirements" }, "includes": { "$ref": "#/$defs/includes" }, "links": { "type": "array", "items": { "$ref": "#/$defs/linkEntry" } } },
"patternProperties": { "^[a-z][a-z0-9-]*$": { "description": "Top-level single-segment key. May be (a) an unknown reserved key not defined in this revision of Section 4 — accepted and preserved (peaceful cohabitation, §3.7); or (b) the first segment of a nested-mapping extension namespace — its subtree is opaque to this schema and MUST be preserved on round-trip. Dotted-string top-level keys are rejected by additionalProperties:false." } },
"additionalProperties": false },
"localizedString": { "description": "BCP 47 language-tagged string map. As a shorthand, a bare string is permitted and treated as { und: <string> } (§7).", "oneOf": [ { "type": "string" }, { "type": "object", "minProperties": 1, "patternProperties": { "^([a-zA-Z]{2,3}|und)(-[a-zA-Z0-9]+)*$": { "type": "string" } }, "additionalProperties": false } ] },
"uri": { "type": "string", "format": "uri" },
"reverseDns": { "type": "string", "pattern": "^[a-z][a-z0-9-]*(\\.[a-z0-9-]+)+$" },
"dnsLabel": { "type": "string", "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?$" },
"version": { "type": "string", "minLength": 1, "description": "Project version in the project's own scheme. SemVer 2.0.0 is RECOMMENDED, not required (spec §4.3).", "examples": ["1.0.0", "2.1.3-rc.1", "24.04", "16.2", "2026.10.1"] },
"semverRange": { "type": "string", "description": "SemVer-style range expression. Advisory pattern; consumers should consult the language-native manifest for authoritative resolution.", "pattern": "^[\\^~><=*xXvV0-9. |&()\\-,]+$", "examples": [">=1.0.0", "^2.1", "~3.14", "1.x", ">=8.5 <9"] },
"relativePath": { "type": "string", "description": "Relative path inside project directory; MUST NOT be absolute, contain '..' segments, or include a Windows drive letter.", "pattern": "^(?![/\\\\])(?![A-Za-z]:[\\\\/])(?!.*\\.\\.).+$" },
"keyword": { "type": "string", "description": "Bare or namespaced (prefix:value) keyword. See spec §4.6.", "pattern": "^[a-z0-9]([a-z0-9-]| [a-z0-9])*(?::[a-z0-9]([a-z0-9-]| [a-z0-9])*)?$" },
"isoDate": { "type": "string", "description": "ISO-8601 calendar date string in YYYY-MM-DD form.", "pattern": "^\\d{4}-\\d{2}-\\d{2}$" },
"identity": { "type": "object", "properties": { "namespace": { "$ref": "#/$defs/reverseDns" }, "name": { "$ref": "#/$defs/dnsLabel" }, "version": { "$ref": "#/$defs/version" }, "title": { "$ref": "#/$defs/localizedString" }, "summary": { "$ref": "#/$defs/localizedString" }, "description": { "$ref": "#/$defs/localizedString" }, "created": { "$ref": "#/$defs/isoDate" }, "released": { "$ref": "#/$defs/isoDate" }, "modified": { "$ref": "#/$defs/isoDate" }, "visibility": { "$ref": "#/$defs/visibility" } }, "additionalProperties": true },
"visibility": { "type": "string", "enum": ["public", "private"], "default": "private", "description": "Whether the project is meant for public distribution. Absence resolves to private; the default governs local derivation only, never a change to remote state (spec §4.3).", "examples": ["public", "private"] },
"repositories": { "type": "array", "minItems": 1, "description": "Source-control clone endpoints. Each entry has a url (a fragment MAY omit it: its origin entry merges into the including document’s, spec §4.9) and optional type/path/branch; role categorizes it (origin/mirror/archive, open vocab) and the reserved value role=origin marks the canonical entry, issues=true marks the forge used for issue tracking, packages=true the one package registries name as the source. Exactly one entry MUST carry role=origin when count > 1; a sole entry is implicitly origin. At most one entry MAY have issues=true, and at most one packages=true. url accepts any scheme git accepts — https, ssh (git@host:path), git+ssh, etc. The forge landing page is NOT a repositories[] value; it goes in links[type=source-code]. See spec §4.3a.", "items": { "type": "object", "properties": { "url": { "type": "string", "minLength": 1, "description": "Clone endpoint. Any scheme git accepts — not constrained to http(s)." }, "type": { "type": "string", "examples": ["git", "hg", "svn", "fossil", "bzr"] }, "path": { "type": "string" }, "branch": { "type": "string" }, "issues": { "type": "boolean", "description": "True for the repository whose forge issue tracker is the project's canonical bug tracker. At most one entry MAY carry issues = true. When absent on all entries, the origin entry is used." }, "packages": { "type": "boolean", "description": "True for the repository that package registries name as the project's source. At most one entry MAY carry packages = true. When absent on all entries, the origin entry is used." }, "role": { "type": "string", "description": "Open vocabulary; recommended values: origin, mirror, archive. The value origin is reserved: it marks the canonical entry (exactly one when the array has more than one element; the sole element is implicitly origin).", "examples": ["origin", "mirror", "archive"] } }, "additionalProperties": true }, "oneOf": [ { "description": "Sole entry — implicitly origin; no origin-cardinality constraint applies.", "maxItems": 1 }, { "description": "Two or more entries — exactly one MUST carry role = origin; at most one MAY have issues = true, and at most one packages = true.", "minItems": 2, "contains": { "type": "object", "properties": { "role": { "const": "origin" } }, "required": ["role"] }, "minContains": 1, "maxContains": 1, "allOf": [ { "contains": { "type": "object", "properties": { "issues": { "const": true } }, "required": ["issues"] }, "minContains": 0, "maxContains": 1 }, { "contains": { "type": "object", "properties": { "packages": { "const": true } }, "required": ["packages"] }, "minContains": 0, "maxContains": 1 } ] } ] },
"license": { "type": "object", "required": ["spdx"], "properties": { "spdx": { "type": "string", "description": "SPDX license expression.", "minLength": 1 }, "covers": { "type": "string", "description": "What corpus the SPDX expression applies to. 'project' (default) covers the whole source tree; 'additions' covers only original/packaging work in this repository, while bundled upstream software retains its own licensing. See spec §4.4.", "enum": ["project", "additions"], "default": "project" }, "file": { "description": "Relative path(s) to license file(s). Single string or sequence of strings. Each path MUST be inside the project directory.", "oneOf": [ { "$ref": "#/$defs/relativePath" }, { "type": "array", "minItems": 1, "uniqueItems": true, "items": { "$ref": "#/$defs/relativePath" } } ] } }, "additionalProperties": true },
"upstream": { "type": "array", "description": "Third-party software the project packages or forks. See spec §4.4b.", "items": { "type": "object", "required": ["name"], "properties": { "name": { "$ref": "#/$defs/localizedString" }, "url": { "type": "string", "format": "uri" }, "version": { "type": "string" }, "license": { "type": "string", "description": "SPDX license expression of the upstream." }, "authors": { "$ref": "#/$defs/localizedString" }, "relation": { "type": "string", "description": "Open vocabulary: packages (default), fork." } } } }, "copyright": { "type": "object", "description": "Top-level copyright block. Holder identities live under `people` with the `copyright` role; per-person `from` / `to` supply the per-holder date range. See spec §4.4a.", "properties": { "year": { "type": "integer", "description": "Default copyright year, applied when a copyright-role person has no `from` date.", "minimum": 1900, "maximum": 9999 } }, "additionalProperties": true },
"person": { "type": "object", "required": ["family-names"], "description": "A person entry in `people[]`. Persons only — entities go in `organizations[]`. See spec §4.5.", "properties": { "family-names": { "type": "string", "minLength": 1, "description": "Family name(s). Multi-part surnames are a single string." }, "given-names": { "type": "string", "minLength": 1 }, "name-particle": { "type": "string", "minLength": 1, "description": "Particle, e.g. 'van', 'van der', 'de', 'von'." }, "name-suffix": { "type": "string", "minLength": 1, "description": "Generational or honorific suffix, e.g. 'Jr.', 'III', 'PhD'." }, "alias": { "type": "string", "minLength": 1 }, "display-name": { "type": "string", "minLength": 1, "description": "Producer-supplied flat name. Consumers emitting to single-name formats MUST prefer this over any synthesized form. See spec §4.5.2." }, "email": { "type": "string", "format": "email" }, "url": { "$ref": "#/$defs/uri" }, "handles": { "$ref": "#/$defs/handles" }, "affiliation": { "type": "string", "minLength": 1 }, "orcid": { "type": "string", "pattern": "^[0-9]{4}-[0-9]{4}-[0-9]{4}-[0-9]{3}[0-9X]$" }, "from": { "$ref": "#/$defs/isoDate", "description": "First date this person was associated with the project. For copyright-role entries the year contributes to assembled copyright lines (§4.4a)." }, "to": { "$ref": "#/$defs/isoDate", "description": "Last date this person was associated with the project. Absent means 'present'." }, "pronouns": { "type": "string", "minLength": 1, "description": "Personal pronouns, e.g. 'she/her', 'he/him', 'they/them'." }, "roles": { "$ref": "#/$defs/roles" } }, "additionalProperties": true },
"organization": { "type": "object", "required": ["name"], "description": "An organization or non-person entity in `organizations[]`. See spec §4.5a.", "properties": { "name": { "type": "string", "minLength": 1, "description": "Organization or entity name." }, "alias": { "type": "string", "minLength": 1 }, "email": { "type": "string", "format": "email" }, "url": { "$ref": "#/$defs/uri" }, "handles": { "$ref": "#/$defs/handles" }, "orcid": { "type": "string", "pattern": "^[0-9]{4}-[0-9]{4}-[0-9]{4}-[0-9]{3}[0-9X]$" }, "from": { "$ref": "#/$defs/isoDate", "description": "First date this organization was associated with the project. For copyright-role entries the year contributes to assembled copyright lines (§4.4a)." }, "to": { "$ref": "#/$defs/isoDate", "description": "Last date this organization was associated with the project. Absent means 'present'." }, "roles": { "$ref": "#/$defs/roles" } }, "additionalProperties": true },
"roles": { "type": "array", "uniqueItems": true, "items": { "type": "string", "examples": ["author", "maintainer", "founder", "contributor", "reviewer", "funder", "packager", "security", "community", "copyright"] } },
"handles": { "type": "object", "description": "Platform handles and usernames. Open-vocabulary map from platform name to the handle or identifier on that platform (e.g. 'signal', 'matrix', 'mastodon'). Each value MAY be a string, an array of strings (multiple handles on the same platform), or a map of strings (named sub-keys, e.g. a GPG key with distinct messaging/code fingerprints).", "additionalProperties": { "$ref": "#/$defs/handleValue" } },
"handleValue": { "description": "A single platform handle value. String for the common case, array of strings when a platform has multiple handles, or map of strings for named sub-keys.", "oneOf": [ { "type": "string", "minLength": 1 }, { "type": "array", "items": { "type": "string", "minLength": 1 }, "minItems": 1, "uniqueItems": true }, { "type": "object", "additionalProperties": { "type": "string", "minLength": 1 }, "minProperties": 1 } ] },
"linkEntry": { "type": "object", "required": ["type", "url"], "description": "A typed navigational URL. See spec §4.10.", "properties": { "type": { "type": "string", "minLength": 1, "description": "URL category. Open vocabulary; consumers MUST NOT reject for unknown types. See spec §4.10 for recommended values.", "examples": [ "homepage", "source-code", "bugs", "documentation", "changelog", "chat", "forum", "contact", "translate", "donation", "security-policy", "security-report", "security-severity", "security-acknowledgments", "bug-bounty", "pgp-key", "openssf-best-practices", "cla", "first-contribution", "enforcement", "conduct-full-text", "paid-support", "status-page", "zenodo-archive", "doi-landing", "governance", "roadmap", "renovate-dashboard", "ci", "package-registry" ] }, "url": { "$ref": "#/$defs/uri" }, "label": { "$ref": "#/$defs/localizedString" }, "preferred": { "type": "boolean", "description": "Marks the canonical entry of its type when more than one entry shares the same type. At most one entry per type MAY carry preferred = true. See spec §4.10." }, "derived": { "type": "boolean", "description": "True when this link was auto-generated by a producer (e.g. the projectfile CLI's derive pass). Consumers MUST treat derived and hand-written links identically; the flag exists so producers can track ownership and avoid overwriting user-edited entries. See spec §4.10." } }, "additionalProperties": true },
"technology": { "type": "string", "description": "Recommended values are listed in spec/technologies.yaml. The vocabulary is non-normative (§4.7) — consumers MUST NOT reject documents for tags outside the recommended list.", "pattern": "^[a-z0-9][a-z0-9.+-]*$" },
"requirements": { "type": "object", "properties": { "browsers": { "description": "Browser targets as Browserslist query strings (§4.8). A flat sequence applies to all environments; a mapping of environment names to query arrays generates per-environment [section] headers in .browserslistrc.", "oneOf": [ { "type": "array", "uniqueItems": true, "items": { "type": "string", "minLength": 1 } }, { "type": "object", "minProperties": 1, "patternProperties": { "^[a-z][a-z0-9-]*$": { "type": "array", "uniqueItems": true, "items": { "type": "string", "minLength": 1 } } }, "additionalProperties": false } ] }, "runtime": { "type": "object", "patternProperties": { "^[a-z][a-z0-9-]*$": { "$ref": "#/$defs/semverRange" } }, "additionalProperties": false } }, "additionalProperties": true },
"includes": { "type": "array", "items": { "oneOf": [ { "type": "string", "minLength": 1, "description": "Relative path or HTTP(S) URL referencing a projectfile document to deep-merge before reading. Resolution is recursive: an included document MAY itself declare includes; relative paths resolve against the including document's directory; cycles MUST be rejected. See spec §4.9." }, { "type": "object", "required": ["url", "sha256"], "properties": { "url": { "type": "string", "pattern": "^https?://", "description": "HTTP(S) URL of the included document." }, "sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$", "description": "Lowercase hex SHA-256 of the fetched bytes. Consumers MUST reject a mismatch (spec §4.9)." } }, "description": "A pinned remote include (spec §4.9)." } ] }, "description": "Documents to deep-merge into the base document. Later entries win over earlier ones; the base document always wins. Includes resolve recursively; cycles are rejected. See spec §4.9." } }}