CycloneDX formulation is the part of a CycloneDX bill of materials that records how a component was made rather than what it contains: the workflows, tasks, steps, commands, triggers, inputs, outputs and workspaces of a build or deployment. It arrived in CycloneDX 1.5 on 26 June 2023, and the same object model carries through 1.6 and 1.7 (ECMA-424).
An SBOM answers "what is in this release?" Formulation answers "what happened to produce it?" Both can live in one JSON document, but the CycloneDX project recommends keeping the build record in a separate file linked from the SBOM, because a build record can reveal far more about your pipeline than you want every customer to see. This post covers every object in the model, compares formulation with SLSA provenance, in-toto links, the SPDX 3 Build profile and CycloneDX attestations, then builds, validates, publishes and signs a formulation file for a GitHub Actions release.
Key takeaways
- Formulation is a top-level
formulationarray offormulaobjects. A formula holds workflows; a workflow holds tasks, steps, a trigger, inputs, outputs, workspaces,taskDependenciesandruntimeTopology; a step holds commands, and a command has one text field,executed. - It was introduced in CycloneDX 1.5. Comparing the published 1.5, 1.6 and 1.7 schemas shows identical property names in every formulation object. Later versions changed descriptions, not structure.
- Few fields are required. Workflows and tasks need
bom-ref,uidandtaskTypes; triggers needbom-ref,uidandtype; workspaces needbom-refanduid. - It can hold the declared recipe or the observed run. The difference is in the content (timestamps, resolved versions, executed commands), not in separate object types.
- It is not an attestation. Nothing in the formulation objects says who vouches for the record. Sign it, attest it, or pair it with platform-signed SLSA provenance.
- The main risk is oversharing. Environment variables, interpolated commands, mount paths and raw event payloads all have a natural home in the model. Record an allowlist, never a dump.
- Tooling is thin. Most teams will emit formulation from a CI script and validate it with the CycloneDX CLI.
For background, see MBOM: the manufacturing bill of materials in CycloneDX, CycloneDX attestations (CDXA) vs in-toto, SLSA provenance and Sigstore bundles, how to read a SLSA provenance file field by field, CycloneDX bom-ref, serial number and version explained, CycloneDX versions explained, 1.0 to 1.7 and ECMA-424 and our review of CycloneDX 1.7.
What is CycloneDX formulation?
The CycloneDX 1.6 JSON schema defines the top-level formulation property as describing "how a component or service was manufactured or deployed", expressed through formulas, workflows, tasks and steps. For software, that means the CI pipeline, the build tool invocations, the test and scan stages, packaging and release, plus the tools, services and workspaces those stages used.
OWASP's announcement of CycloneDX 1.5, dated 26 June 2023, introduced the feature as "Formulation (MBOM)", describing it as "comprehensive recipes of how a particular software system was created, trained, or deployed." MBOM (manufacturing bill of materials) is the capability name on the CycloneDX site; formulation is the schema property that carries it.
The CycloneDX Authoritative Guide to MBOM, first edition, published 21 October 2025 by the CycloneDX Core Working Group, widens the scope on purpose. It presents formulation as a way to describe "how anything came to be": software builds, hardware manufacturing, service deployment, data set curation, cryptographic validation, infrastructure provisioning and even the process that generated a BOM. This post stays with software builds.
Timeline
| Date | Event | What it means for formulation |
|---|---|---|
| CycloneDX 1.4 | No formulation property | Build details had to be squeezed into properties or external references |
| 26 June 2023 | OWASP announces CycloneDX 1.5 | formulation added with formula, workflow, task, step, command, trigger, event, condition, workspace, volume, inputType and outputType |
| 9 April 2024 | CycloneDX 1.6 released, introducing attestations (CDXA) | Formulation property names unchanged; the shared dependency type gains provides |
| June 2024 | ECMA-424 1st edition | CycloneDX becomes an Ecma standard |
| 21 October 2025 | Authoritative Guide to MBOM, 1st edition | Official modelling guidance with a worked C and Makefile example |
| December 2025 | ECMA-424 2nd edition, defining CycloneDX 1.7 | Property names unchanged; descriptions expanded, for example string-only environment variables documented as flags |
We checked the "unchanged" claims directly by loading the 1.5, 1.6 and 1.7 schemas and comparing the property lists of every formulation definition and the taskType enumeration. They match. A 1.5 formulation is structurally valid 1.7 once you change specVersion.
How "how it was made" differs from the inventory
A conventional SBOM is an inventory: components lists what ships, and dependencies says what depends on what. Formulation is a process record. Its components and services are things used to make the product but not shipped in it: compiler, runner image, package registry, test framework, signing service. The schema calls these transient.
| Question | Component inventory (SBOM) | Formulation |
|---|---|---|
| Where it lives | components, dependencies | formulation |
| What it lists | What ships in the product | What made the product, and the process |
| Typical reader | Vulnerability and licence tooling, procurement | Auditors, build engineers, incident responders |
| Changes when | Dependencies change | Pipeline, tools, runners or triggers change, even if dependencies do not |
| Sensitivity | Moderate | High: pipeline layout, paths, sometimes credentials if done badly |
The CycloneDX MBOM capability page speaks of "declared and observed formulation for reproducibility throughout the product lifecycle." A declared formulation is the recipe the pipeline is configured to follow. An observed formulation is what one run actually did: timestamps, resolved tool versions, commands with variables substituted. Comparing the two is how you detect drift.
The formulation data model, field by field
Every name below comes from the CycloneDX 1.6 JSON schema; 1.5 and 1.7 use the same names. The hierarchy is a chain: formulation holds formulas, formulas hold workflows, workflows hold tasks, and tasks hold steps.
formula
formulation is an array of formula objects. A formula has five optional properties: bom-ref; components (transient tools and images); services (registries, artifact stores, deployment targets); workflows; and properties, the name and value escape hatch.
The MBOM guide adds a rule the schema cannot enforce: a formula is only valid if every component and service it references is declared in the same BOM document. In practice, every ref inside a task, input, output or workspace must resolve to a bom-ref in the same file. Use several formulas when one product is made several ways, for example a container image and a native installer built by different pipelines.
workflow
The guide maps workflows to CI pipelines and describes them as directed acyclic graphs of dependent tasks. Required: bom-ref, uid, taskTypes. The rest:
| Field | Purpose |
|---|---|
uid | Identifier of this instance in its deployment context, for example run ID plus attempt |
name, description | Labels |
resourceReferences | Components or services that realise the workflow, such as the CI service |
tasks, taskDependencies | The tasks and the order between them |
trigger | What started it |
steps | Steps attached directly to the workflow |
inputs, outputs | Data and resources in and out |
timeStart, timeEnd | Timestamps for an observed run |
workspaces | Shared filesystems or data resources |
runtimeTopology | How the runtime components were wired for this instance |
properties | Extensions |
taskTypes has had twelve values since 1.5: copy, clone, lint, scan, merge, build, test, deliver, deploy, release, clean, other. The guide calls them informational labels. A task labelled scan proves nothing about whether a scan ran.
task
A task is a unit of work; in GitHub Actions terms it maps most naturally to a job. It has the same properties as a workflow except tasks and taskDependencies, with the same required fields. That symmetry means a simple pipeline can be one workflow with steps, and a complex one can be a graph of tasks with their own inputs and outputs.
step and command
A step runs commands or tools, in sequence, for the task it belongs to. It has name, description, commands and properties. A command has exactly two properties: executed, a text form of the command that ran, and properties. The guide's example is an interpolated shell command, one with its variables already substituted. There is no field for exit code or duration; use properties or a log output. And note that an interpolated command is exactly where a token leaks if a token was on the command line.
trigger, event and condition
A trigger fires tasks when an associated event, and its data, meets the trigger's conditions. Required: bom-ref, uid, type. type is one of manual, api, webhook or scheduled. Other fields: event, conditions, timeActivated, inputs, outputs, resourceReferences, name, description, properties.
An event has uid, description, timeReceived, data (an attachment for the raw payload), source, target and properties. A condition has description, expression (the logical expression evaluated to decide whether to fire) and properties.
The specification does not prescribe how CI events map to trigger types. For GitHub Actions, a reasonable mapping is workflow_dispatch to manual, schedule to scheduled, repository_dispatch to api, and push, pull_request and release to webhook. Write your mapping down and keep it stable, or run comparisons will show false drift. Think twice before filling event.data: a push payload carries committer emails, a pull request payload carries text written by outsiders.
inputs and outputs
An inputType has source, target, resource, parameters, environmentVars, data and properties, wrapped in a oneOf that requires exactly one of resource, parameters, environmentVars or data. An input carrying both resource and parameters fails validation; we confirmed this against the 1.6 schema. Use one input object per kind.
An outputType has type, source, target, resource, data, environmentVars and properties, with a oneOf over resource, environmentVars or data. Output type is one of artifact, attestation, log, evidence, metrics or other. An attestation output is a clean way to say "this task produced a signed provenance file" and point at it.
resource, source and target use resourceReferenceChoice: either ref (a bom-ref here, or a BOM-Link into another BOM) or externalReference (a URL with optional hashes). That is how a build task's output points at the component the SBOM describes. environmentVars items are either name and value pairs or bare strings; the 1.7 schema explains the string form as variables that "consist of a single string without an explicit value assignment", typically flags.
workspaces and volumes
A workspace is a named filesystem or data resource that tasks can share. Required: bom-ref, uid. Fields include aliases, accessMode, mountPath, managedDataType, volumeRequest, volume and resourceReferences. accessMode is one of read-only, read-write, read-write-once, write-once, write-only, which is useful in review: a cache workspace shared between pull request and release workflows deserves a question.
A volume records uid, name, mode (filesystem or block), path, sizeAllocated, persistent and remote. persistent: true on a build workspace records that state survived between runs, which matters for both reproducibility and cache poisoning analysis.
taskDependencies and runtimeTopology
Both reuse the CycloneDX dependency type (ref, dependsOn, and since 1.6 provides). taskDependencies sits on workflows and records execution order between tasks. runtimeTopology sits on workflows and tasks and describes how runtime components were wired, for example runner image on container runtime on host.
Required fields at a glance
| Object | Required | Enumerations |
|---|---|---|
| workflow, task | bom-ref, uid, taskTypes | taskTypes: twelve values listed above |
| trigger | bom-ref, uid, type | type: manual, api, webhook, scheduled |
| workspace | bom-ref, uid | accessMode: five values listed above |
| inputType | exactly one of resource, parameters, environmentVars, data | none |
| outputType | exactly one of resource, environmentVars, data | type: artifact, attestation, log, evidence, metrics, other |
| formula, step, command, event, condition, volume | none | volume.mode: filesystem, block |
Inline or linked: where formulation should live
You can put formulation in the same document as the inventory, but the MBOM guide recommends against it as a default. Its section on composability argues that you can share component inventories widely while keeping tighter control over the more sensitive process details in formulation. The recommended pattern is a separate CycloneDX document referenced from the SBOM with an external reference of type formulation (one of 43 reference types in the 1.6 schema), with a hash so readers can confirm they fetched the right file:
"externalReferences": [
{
"type": "formulation",
"url": "https://example.com/mboms/acme-library-1.0.cdx.json",
"hashes": [ { "alg": "SHA-256", "content": "c7be1ed902fb8dd4d48997c6452f5d7e509fbcdbe2808b16bcf4edce4c07d14e" } ]
}
]
The split gives separate access control, separate lifecycles (a pipeline change does not force a new SBOM version) and smaller files. The cost is a second fetch and keeping the hash current.
How formulation differs from SLSA provenance, in-toto, SPDX 3 Build and CDXA
SLSA provenance
SLSA build provenance is an in-toto attestation predicate with type https://slsa.dev/provenance/v1. The SLSA site lists v1.2 as the approved specification and marks the v1.0 and v1.1 provenance pages as retired; the predicate type string stays the same across those minor versions.
The predicate has buildDefinition (buildType, externalParameters, internalParameters, resolvedDependencies) and runDetails (builder.id, metadata with invocationId, startedOn, finishedOn, and byproducts). External parameters "MUST be complete at SLSA Build L3", and the accuracy of the build definition rests on the identity in builder.id. The SLSA build track sets the progression: provenance exists at Build L1 but may be unsigned; Build L2 requires signed provenance generated by a hosted build platform; Build L3 requires a hardened build platform.
Provenance is deliberately terse; the specification asks platforms to keep external parameters small so verification stays simple. Formulation goes the other way and can describe every task, step and command. They complement each other: provenance is the short, signed answer to "which trusted builder made this from which inputs", formulation the long, descriptive answer to "what happened inside". The MBOM guide lists SLSA compliance among MBOM's roles because formulation can carry that detail, but a SLSA verifier expects an in-toto statement with the SLSA predicate, not a CycloneDX BOM.
in-toto link attestations
The in-toto Link predicate (https://in-toto.io/attestation/link/v0.3) carries name, command, materials, byproducts and environment, with the statement's subject as the products. Each link covers the execution of "one step in the software supply chain", and every step in a supply chain layout needs one or more signed links, depending on the layout's threshold. An in-toto link is roughly one signed CycloneDX task, checked against a layout that says which steps must happen, in what order, performed by whom. Formulation has ordering but no per-task signer.
SPDX 3 Build profile
SPDX 3.0.1's Build profile defines the information needed to describe one software build instance. It has one class, Build, and uses the relationships hasInput, hasOutput and invokedBy. Build has buildType (required, a URI), buildId, buildStartTime, buildEndTime, configSourceDigest, configSourceEntrypoint, configSourceUri, environment and parameter. The class description notes the times are optional so they can be left out to simplify reproducible builds. Its shape is closer to SLSA provenance (one build, a type URI, inputs and outputs) than to formulation's nested workflows. If you exchange SPDX 3, use it; if you exchange CycloneDX, use formulation.
CycloneDX attestations (CDXA)
CDXA arrived in 1.6 through the top-level declarations property (assessors, attestations, claims, evidence, targets, an affirmation with signatories, and a signature). The CycloneDX attestations page describes it as a way to "represent claims about adherence to specific requirements, substantiated with evidence and supporting documentation." Formulation describes a process; CDXA claims a requirement was met. They fit: a claim such as "release builds run on ephemeral runners" can cite a formulation document whose workspaces show persistent: false.
Comparison table
| CycloneDX formulation | SLSA provenance v1 | in-toto link v0.3 | SPDX 3.0.1 Build | CycloneDX CDXA | |
|---|---|---|---|---|---|
| Describes | Whole process: workflows, tasks, steps, triggers, workspaces, tools | One build: builder, external parameters, resolved dependencies | One step: command, materials, products, environment | One build: type, config source, parameters, environment | Claims of conformance backed by evidence |
| Envelope | CycloneDX BOM | in-toto statement | in-toto statement | SPDX 3 document | CycloneDX BOM |
| Who signs | Nobody by default; optional JSF signature on the BOM, or sign externally | The build platform named in builder.id | The functionary who ran the step | Nobody by default; sign externally | Declared signatories; optional BOM signature |
| Trust model | Self-reported unless signed or cross-checked | Trust the builder, check expected parameters | Check each link against a layout and its keys | Self-reported unless signed | Trust the signatory and the evidence |
| Verifier support | Schema validation; no common policy verifier | Several open source verifiers and platform commands | in-toto reference implementations | SPDX parsing and validation tools | Schema validation; early tooling |
| Best for | Audit detail, reproducibility notes, drift detection | Gating on "built by trusted CI from this source" | Proving multi-party steps happened in order | SPDX-native build records | Compliance statements that cite build evidence |
Which should you produce?
| Goal | Produce first | Add |
|---|---|---|
| Reject artifacts not built by your CI | SLSA provenance from the platform | Formulation for the detail |
| Show an auditor that tests and scans ran on release builds | Observed formulation, signed | A CDXA claim citing it |
| Prove a multi-party process ran in order | in-toto links and a layout | Formulation for human readers |
| Detect pipeline drift between releases | Observed formulation per run | Nothing else internally |
What formulation is for
Reproducible builds. The MBOM guide says formulation's level of detail could potentially give someone enough to reproduce a component's manufacturing process independently, and "potentially" is honest. Bit for bit reproducibility needs fixed timestamps and stable ordering; formulation helps by recording the ingredients someone needs to try: exact tool versions, executed commands, parameters and the environment variables that matter. A good example is SOURCE_DATE_EPOCH, which Reproducible Builds describes as "a standardised environment variable that distributions can set centrally" so tools use a fixed timestamp. Record it as an input. Do not embed the formulation inside the artifact you want reproducible, because its run timestamps change every time.
Auditor and customer evidence. Questionnaires ask you to describe your pipeline and how release builds are tested and scanned. Formulation answers with data that can be sampled: the release workflow, its tasks, tools and versions, the trigger, the outputs. It is self-reported, so sign it and cross-check it (see risks), but it is far easier to check than prose.
EU Cyber Resilience Act technical documentation. Regulation (EU) 2024/2847 requires manufacturers of products with digital elements to draw up technical documentation, and Annex VII lists what it must contain. Read Annex VII directly for the wording on design, development and production. We are not aware of the regulation naming any BOM format, and it does not require CycloneDX formulation. Treat formulation as supporting material for the production side, next to an SBOM for the inventory, and check the current text and implementing acts before relying on it.
Detecting build environment drift. If every release emits an observed formulation, diff consecutive runs and flag unannounced changes: a tool version moved, a new service appeared, a workspace became persistent, a task disappeared, a release trigger changed from webhook to manual. None of that shows in the SBOM if dependencies did not change. This works best when the same generator, with stable property names, runs every time.
Beyond software. The guide also covers hardware manufacturing, data pipelines and model training, quantization and deployment. The practical point for software teams is one vocabulary for both the build of the application and the pipeline behind a model it ships.
Which tools generate or consume formulation today?
The CycloneDX tool center lets listings declare capabilities, and "Formulation (MBOM)" is one filter. On 24 October 2026, eleven listings carried that tag. Most are CycloneDX's own projects: the CycloneDX CLI (analysis, merge, diff, conversion, signing and validation, listed for CycloneDX 1.0 to 1.7), the .NET, JavaScript and Python libraries, and the web tool. The rest are a few community libraries, signing tools, pipeline utilities and one commercial platform.
Two cautions. Tags are self-declared, so a tag means the tool can represent or process formulation, not that it generates a complete build record for you. Check each project's documentation before assuming automatic emission. In practice, the libraries give you typed models for writing your own generator, and the CLI validates the result.
So the realistic path is a CI script that gathers allowlisted facts the CI system already exposes and writes them into a formulation document. GitHub Actions, for example, exposes GITHUB_RUN_ID ("A unique number for each workflow run within a repository"), GITHUB_RUN_ATTEMPT, GITHUB_SHA, GITHUB_EVENT_NAME, RUNNER_OS and RUNNER_ARCH. On the consumption side, few tools do more than parse and validate formulation, which is a reason to produce it for your own drift detection, incident response and audit evidence first.
Risks of publishing build records
Secrets and internal details. The natural slots for leaks are environmentVars, commands[].executed (interpolated), event.data (raw payloads with names and emails), mountPath, volume.path, service endpoints and free-form properties. Build the document from an explicit allowlist; never iterate over the environment. Pass secrets through files or environment variables, not arguments, and record a variable's name with a redacted value if presence matters. Strip event.data unless you need it. Keep formulation separate from the SBOM with a narrower audience, and check the generated file for credentials before it leaves the pipeline, as you would any published build output.
Self-reported data. A formulation says what its author wrote. If a compromised build step can write the file, it can write a clean story. That is the core difference from SLSA provenance at Build L2 and above, where a hosted platform generates and signs the provenance, and at L3 the platform is hardened against tampering during the build. Narrow the gap: generate formulation in a separate job that reads only platform-exposed facts, and cross-check the artifact hash in the build task's output against the subject digest in the platform-signed provenance. Treat a mismatch as an incident.
Unsigned documents. A file attached to a release can be swapped. CycloneDX supports an optional enveloped JSON Signature Format signature on the BOM, the CycloneDX CLI can produce detached RSA signatures, and a platform attestation can bind the file's digest to a build identity. Pick one and document how to verify it.
Partial records. A formulation with three tasks for a nine-job pipeline looks complete and is not. Make the generator fail when a known job is missing, and consider the BOM's compositions property, whose aggregate values include complete, incomplete and unknown, to state completeness explicitly.
Step by step: a formulation for a GitHub Actions release
This walkthrough produces an observed formulation for a small npm package release, validates it against CycloneDX 1.6, links it from the SBOM, signs it and attaches everything to the release. The names are illustrative.
Step 1: install the CycloneDX CLI
The README lists release binaries, a Docker image and a Homebrew tap. The latest release when we checked was v0.33.1, published 23 July 2026; its validate command accepts versions v1_0 to v1_7 and defaults to v1.7.
brew install cyclonedx/cyclonedx/cyclonedx-cli
# or, without installing
docker run --rm -v "$PWD:/work" -w /work cyclonedx/cyclonedx-cli --help
Step 2: write a minimal formulation by hand
Write the first one by hand so you understand every field before automating it. This describes one workflow with checkout, build and test tasks, the tools and registry they used, the workspace and the outputs. We validated it against the published 1.6 schema with zero errors and confirmed every ref resolves.
{
"bomFormat": "CycloneDX",
"specVersion": "1.6",
"serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79",
"version": 1,
"metadata": {
"timestamp": "2026-10-24T09:14:05Z",
"component": {
"type": "library",
"bom-ref": "pkg:npm/acme-widget@2.4.0",
"name": "acme-widget",
"version": "2.4.0",
"purl": "pkg:npm/acme-widget@2.4.0"
}
},
"formulation": [
{
"bom-ref": "formula-release",
"components": [
{ "type": "platform", "bom-ref": "runner-image", "name": "ubuntu", "version": "24.04" },
{ "type": "application", "bom-ref": "tool-node", "name": "node", "version": "22.11.0" },
{ "type": "application", "bom-ref": "tool-npm", "name": "npm", "version": "10.9.0" },
{
"type": "file", "bom-ref": "src-lockfile", "name": "package-lock.json",
"hashes": [ { "alg": "SHA-256", "content": "9f2c1b7e4a6d3c8b5e0f1a2d3c4b5a6978e1d2c3b4a5968778695a4b3c2d1e0f" } ]
}
],
"services": [
{ "bom-ref": "svc-ci", "name": "GitHub Actions" },
{ "bom-ref": "svc-npm-registry", "name": "npm public registry", "endpoints": [ "https://registry.npmjs.org" ] }
],
"workflows": [
{
"bom-ref": "wf-release",
"uid": "release.yml/run-16588214930/attempt-1",
"name": "release",
"taskTypes": [ "clone", "build", "test", "release" ],
"resourceReferences": [ { "ref": "svc-ci" } ],
"timeStart": "2026-10-24T09:10:02Z",
"timeEnd": "2026-10-24T09:14:01Z",
"trigger": {
"bom-ref": "trigger-tag-push",
"uid": "push/refs/tags/v2.4.0",
"type": "webhook",
"name": "tag push",
"timeActivated": "2026-10-24T09:10:00Z",
"event": {
"uid": "github-event-push-v2.4.0",
"description": "push of tag v2.4.0",
"timeReceived": "2026-10-24T09:09:59Z"
},
"conditions": [
{ "description": "release tags only", "expression": "startsWith(github.ref, 'refs/tags/v')" }
]
},
"workspaces": [
{
"bom-ref": "ws-checkout",
"uid": "github-workspace",
"name": "GITHUB_WORKSPACE",
"accessMode": "read-write",
"mountPath": "/home/runner/work/acme-widget/acme-widget",
"volume": { "mode": "filesystem", "persistent": false, "remote": false }
}
],
"tasks": [
{
"bom-ref": "task-checkout",
"uid": "job-checkout",
"name": "checkout",
"taskTypes": [ "clone" ],
"steps": [
{ "name": "checkout", "commands": [ { "executed": "git checkout --detach 4f1d2a9c7e3b5a6d8c0e1f2a3b4c5d6e7f8a9b0c" } ] }
],
"outputs": [ { "type": "artifact", "resource": { "ref": "src-lockfile" } } ]
},
{
"bom-ref": "task-build",
"uid": "job-build",
"name": "build",
"taskTypes": [ "build" ],
"resourceReferences": [ { "ref": "tool-node" }, { "ref": "tool-npm" }, { "ref": "runner-image" } ],
"inputs": [
{ "resource": { "ref": "src-lockfile" } },
{ "source": { "ref": "svc-npm-registry" }, "data": { "contentType": "text/plain", "content": "dependencies resolved from lockfile" } },
{ "environmentVars": [ { "name": "SOURCE_DATE_EPOCH", "value": "1761296999" }, { "name": "NODE_ENV", "value": "production" } ] }
],
"steps": [
{ "name": "install", "commands": [ { "executed": "npm ci --ignore-scripts" } ] },
{ "name": "build", "commands": [ { "executed": "npm run build" } ] },
{ "name": "pack", "commands": [ { "executed": "npm pack" } ] }
],
"outputs": [ { "type": "artifact", "resource": { "ref": "pkg:npm/acme-widget@2.4.0" } } ]
},
{
"bom-ref": "task-test",
"uid": "job-test",
"name": "test",
"taskTypes": [ "test" ],
"steps": [ { "name": "unit tests", "commands": [ { "executed": "npm test" } ] } ],
"outputs": [ { "type": "log", "data": { "contentType": "text/plain", "content": "212 passed, 0 failed" } } ]
}
],
"taskDependencies": [
{ "ref": "task-build", "dependsOn": [ "task-checkout" ] },
{ "ref": "task-test", "dependsOn": [ "task-build" ] }
]
}
]
}
]
}
Notice that the build task's output points at pkg:npm/acme-widget@2.4.0, the metadata component's bom-ref, linking process to product. Each input carries exactly one alternative. Only two environment variables are recorded, both deliberately. The install step runs npm ci with lifecycle scripts disabled, and the record shows it. The workspace is persistent: false.
Step 3: validate it
cyclonedx validate --input-file formulation.cdx.json --input-version v1_6 --fail-on-errors
This catches a task without uid, a trigger.type of push, or an input with two alternatives. It does not check that every ref resolves, so add your own check:
jq -r '.. | objects | .ref? // empty' formulation.cdx.json | sort -u > refs.txt
jq -r '.. | objects | ."bom-ref"? // empty' formulation.cdx.json | sort -u > declared.txt
comm -23 refs.txt declared.txt # empty output means every ref resolves
Step 4: generate it in CI from an allowlist
Keep the hand-written file as a template and fill the observed fields from named variables. The workflow never iterates over the environment.
name: release
on:
push:
tags: [ "v*" ]
permissions:
contents: write
id-token: write
attestations: write
jobs:
release:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "22"
- name: Build
run: |
export SOURCE_DATE_EPOCH="$(git log -1 --pretty=%ct)"
npm ci --ignore-scripts
npm run build
npm test
npm pack
- name: Write formulation
run: |
jq \
--arg uid "release.yml/run-${GITHUB_RUN_ID}/attempt-${GITHUB_RUN_ATTEMPT}" \
--arg sha "${GITHUB_SHA}" \
--arg event "${GITHUB_EVENT_NAME}" \
--arg os "${RUNNER_OS}" \
--arg arch "${RUNNER_ARCH}" \
--arg node "$(node --version | sed 's/^v//')" \
--arg npm "$(npm --version)" \
--arg sde "$(git log -1 --pretty=%ct)" \
--arg serial "urn:uuid:$(uuidgen | tr 'A-Z' 'a-z')" \
--arg now "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
'.serialNumber = $serial
| .metadata.timestamp = $now
| .formulation[0].workflows[0].uid = $uid
| .formulation[0].workflows[0].timeEnd = $now
| (.formulation[0].components[] | select(."bom-ref"=="tool-node") | .version) = $node
| (.formulation[0].components[] | select(."bom-ref"=="tool-npm") | .version) = $npm
| .formulation[0].workflows[0].tasks[0].steps[0].commands[0].executed = ("git checkout --detach " + $sha)
| .formulation[0].workflows[0].tasks[1].inputs[2].environmentVars[0].value = $sde
| .formulation[0].workflows[0].properties = [
{"name":"acme:ci:event","value":$event},
{"name":"acme:ci:runner-os","value":$os},
{"name":"acme:ci:runner-arch","value":$arch}
]' \
.ci/formulation.template.json > formulation.cdx.json
- name: Validate formulation
run: |
docker run --rm -v "$PWD:/work" -w /work cyclonedx/cyclonedx-cli \
validate --input-file formulation.cdx.json --input-version v1_6 --fail-on-errors
The build and the record use the same SOURCE_DATE_EPOCH (the tagged commit's time), so they agree. Fill the lockfile hash and start time the same way. For larger pipelines, use one of the CycloneDX libraries instead of jq; typed models make structurally invalid output harder to produce.
Step 5: link it from the SBOM
TAG="v2.4.0"; REPO="acme/acme-widget"
FORM_SHA256=$(sha256sum formulation.cdx.json | cut -d' ' -f1)
FORM_URL="https://github.com/${REPO}/releases/download/${TAG}/formulation.cdx.json"
jq --arg url "$FORM_URL" --arg sha "$FORM_SHA256" \
'.externalReferences = ((.externalReferences // []) + [{
"type": "formulation", "url": $url,
"hashes": [ { "alg": "SHA-256", "content": $sha } ] }])' \
sbom.cdx.json > sbom.linked.cdx.json
If the formulation should not be public, point the URL at an access-controlled location; the hash still lets authorised readers confirm the file.
Step 6: sign it
Option one is a detached signature. The CLI's keygen creates an RSA key pair, sign file writes a PKCS1 RSA SHA256 signature, and verify file checks it. Keep the private key in your CI secret store.
cyclonedx keygen # once, on a secure machine
cyclonedx sign file formulation.cdx.json --key-file private.key # writes formulation.cdx.json.sig
cyclonedx verify file formulation.cdx.json --key-file public.key
Option two is a platform attestation. GitHub's artifact attestation documentation shows actions/attest@v4 with a subject-path input and the id-token: write and attestations: write permissions; gh attestation verify checks the result. Using the formulation file as the subject binds its digest to the workflow run.
- name: Attest formulation file
uses: actions/attest@v4
with:
subject-path: formulation.cdx.json
gh attestation verify formulation.cdx.json -R acme/acme-widget
GitHub's documentation says that on Free, Pro and Team plans, artifact attestations are available only for public repositories; private and internal repositories need Enterprise Cloud. Many teams use both options: the attestation proves the file came from CI, the detached signature is easy for consumers outside the platform.
Step 7: attach and compare
gh release upload v2.4.0 acme-widget-2.4.0.tgz sbom.linked.cdx.json \
formulation.cdx.json formulation.cdx.json.sig --repo acme/acme-widget
# drift: tools and tasks that changed since the previous release
diff <(jq -r '.formulation[].components[] | "\(.name) \(.version)"' previous.cdx.json | sort) \
<(jq -r '.formulation[].components[] | "\(.name) \(.version)"' formulation.cdx.json | sort)
diff <(jq -r '.formulation[].workflows[].tasks[].name' previous.cdx.json | sort) \
<(jq -r '.formulation[].workflows[].tasks[].name' formulation.cdx.json | sort)
Publish the public key, or the repository identity a verifier should expect, in your security documentation.
Common mistakes
- Dumping the environment into
environmentVars. Use an allowlist. - Secrets on command lines.
executedis the interpolated command; pass secrets through files or variables. - Shipping formulation inside the public SBOM by default. Link a separate file and choose each audience.
- Treating
taskTypesas proof. Point a scan task at anevidenceorlogoutput if the record should mean something. - Mixing alternatives in one input.
oneOfrejects it; split the input. - Unresolved references. Schema validation does not catch them; run the
commcheck. - Calling formulation a SLSA attestation. Produce real SLSA provenance when consumers need it.
- Leaving it unsigned, or embedding run timestamps in the artifact you want reproducible.
- Inconsistent trigger mapping between releases, which shows up as false drift.
Where Safeguard fits
Safeguard's fit here is indirect. Safeguard generates and analyses SBOMs in CycloneDX (JSON and XML) and SPDX, scans repositories, container images and existing SBOM files, shows which projects contain a given component and version, matches components against vulnerability databases, ranks findings with reachability analysis, and can open fix pull requests when you switch autonomous remediation on. That is all work on the component inventory. We have not documented Safeguard as reading or writing the formulation array, so treat formulation as something your pipeline produces and stores next to the SBOM. The two meet at the purl your build task outputs: formulation says how the release was made, the inventory scan says whether what went into it is still safe to ship.
FAQ
What is formulation in CycloneDX?
The top-level formulation array that describes how a component or service was made or deployed: formulas containing workflows, tasks, steps and commands, with triggers, inputs, outputs, workspaces and the transient tools and services used.
Which CycloneDX version added formulation?
CycloneDX 1.5, announced 26 June 2023. The 1.4 schema has no formulation property, and formulation property names are identical in 1.5, 1.6 and 1.7.
Is an MBOM the same as formulation?
In CycloneDX, yes in practice. MBOM is the capability name; formulation is the property that carries it.
Is CycloneDX formulation the same as SLSA provenance?
No. SLSA provenance is a platform-made in-toto predicate, https://slsa.dev/provenance/v1, about one build. Formulation is a detailed, unsigned description inside a CycloneDX BOM. SLSA verifiers need the SLSA predicate.
Should formulation go in my SBOM or a separate file?
The CycloneDX MBOM guide recommends a separate document, referenced from the SBOM by an external reference of type formulation with a hash.
What are the required fields in a CycloneDX workflow or task?
bom-ref, uid and taskTypes. Triggers need bom-ref, uid and type; workspaces need bom-ref and uid.
Can formulation leak secrets?
Yes, through environment variables, interpolated commands, raw event payloads, paths and free-form properties. Generate from an allowlist and check the output before publishing.
How do I validate and sign a CycloneDX formulation file?
Validate with the CycloneDX CLI against the matching schema version, then check that every ref resolves. Sign with a detached signature from the CLI, an enveloped JSON Signature Format signature, or a platform attestation over the file.
A formulation file records how a release was built, and that build still pulled open source packages and a runner image from somewhere. When one of those components turns out to be vulnerable, the record helps you find the affected releases, and the next job is getting fixed versions into the pipeline it describes.
Fix it without waiting for upstream
Whatever the vulnerability, most of the risk sits in two gaps. The first is the time between a fix being published and that fix actually running in your production builds. The second, and worse, is the time before any fix exists at all, while a maintainer works out a patch and a release schedule. Waiting through both gaps is a habit, not a requirement. Safeguard gives teams three ways to close them.
1. Autonomous remediation: pull requests, not tickets
When a fixed version exists, Safeguard's Griffin models open a tested, policy gated pull request that moves the affected dependency to the safe version. You can run it on one repository or in bulk across every project that carries the vulnerable version. Remediation covers eight categories, including dependencies, licenses, security posture and findings. Reachability analysis ranks the vulnerable code paths your application actually calls first, so the first pull requests you merge are the ones that matter most. Autonomous remediation ships switched off, with every category set to Manual, so your team decides how much runs on its own and what still needs a human approval.
2. Gold Open Source: the safe version in seconds, free
gold.safeguard.sh is free and needs no login. Search any CVE or package to see affected and fixed versions, CVSS, EPSS, CISA KEV status and a recommended safe version, or use the "Am I affected?" checker. The same data is available through a free read API, README status badges and RSS threat feeds. The safeguard-sh/gold-scan-action GitHub Action can fail a build on a known exploited vulnerability, on any known vulnerability, or never, so a version you have just removed cannot quietly come back in next week's merge.
3. Gold Registry: start from components that are already fixed
registry.safeguard.sh provides hardened, drop in replacements for upstream packages and container images. Every Gold artifact is held continuously to zero exploitable critical or high CVEs, zero malware and a completed license check, with SLSA L3 or higher provenance, a Sigstore signature and a public evidence page you can hand to an auditor. An artifact loses its Gold badge if it regresses. Teams can point their own package directory at the registry so hardened equivalents resolve without changing how developers install anything.
When upstream has no fix yet, or will not ship one, Safeguard first works to get the fix accepted upstream. If the issue is severe or upstream will not move, Safeguard builds a fork, fixes the vulnerability there and publishes a hardened Gold build, so you are not stuck on a maintainer's release schedule. You can also request a hardened, compatibility tested build of a specific package. This fork and patch flow is in early access today, so ask which packages it covers before you plan around it.
| Your situation | What to use |
|---|---|
| A fixed version exists | Autonomous remediation opens the upgrade pull request; Gold Open Source confirms the safe version |
| You need to know today whether you are affected | Gold Open Source lookup and "Am I affected?" checker, free and without login |
| You want vulnerable versions to stop shipping | The Gold scan GitHub Action in CI, plus Safeguard policies and gates |
| Upstream has no fix yet, or will not ship one | A custom Gold request; fork and patch (early access) |
| You want to start clean on new projects | Pull hardened components and images from Gold Registry |
One honest limit: when the vulnerable code lives in a vendor appliance, a hosted service or an operating system runtime you do not build yourself, the fix still has to come from that vendor or distribution. The steps above apply to the open source dependencies and container images you build and ship, which is where most teams carry the largest and least tracked share of their exposure.
See how Safeguard would help
None of these demos generates a formulation document. They show what happens to the components that a formulation and its linked SBOM describe.
The first demo sets up the local runner on your own machine, approves it with its device code, then scans from the terminal. Scanning where your build runs is the inventory step that sits beside the formulation: one says how the release was made, the other what went into it.
Click through it at your own pace, or open it full screen.
The second demo is a real Safeguard CLI session. Asked to fix a lodash CVE, the assistant reads the finding and advisory, asks before each command, edits package.json to lodash 4.17.21, runs npm install and npm test with your approval, prints a summary and pushes nothing. In the pipeline above, that change would appear in the next release's formulation as a new lockfile hash and a passing test task.
Click through it at your own pace, or open it full screen.
The third demo connects Claude to the Safeguard MCP server with OAuth and lets you choose which tools Claude can use. If you already ask an assistant to explain a BOM or a pipeline file, this gives it your Safeguard findings under tool permissions you control.
Click through it at your own pace, or open it full screen.
To try it on your own projects, download Safeguard or read the CLI documentation.
Quick check
A task in a formulation document has taskTypes set to scan. Does that prove a scan ran on the release build?
No. Task types are informational labels. The task needs an evidence or log output pointing at the scan result, and the document should be signed or attested so it cannot be rewritten afterwards.
A customer's admission policy requires SLSA provenance. Can you send a CycloneDX formulation instead?
Not as a substitute. SLSA verifiers expect an in-toto statement with the predicate type https://slsa.dev/provenance/v1, made by the build platform. Send the provenance and offer the formulation as supporting detail.
Sources
- CycloneDX 1.4 JSON schema
- CycloneDX 1.5 JSON schema
- CycloneDX 1.6 JSON schema
- CycloneDX 1.7 JSON schema
- CycloneDX 1.6 JSON reference documentation
- OWASP: Introducing OWASP CycloneDX v1.5, 26 June 2023
- CycloneDX v1.6 released, 9 April 2024
- CycloneDX: Manufacturing Bill of Materials (MBOM) capability
- CycloneDX: Authoritative Guide to MBOM, first edition, 21 October 2025 (PDF)
- CycloneDX: Attestations (CDXA) capability
- CycloneDX tool center
- CycloneDX CLI README
- CycloneDX CLI releases
- CycloneDX bom-examples: MBOM helloworld-c
- Ecma International: ECMA-424
- SLSA v1.2: Build provenance
- SLSA v1.2: Build track basics
- SLSA v1.0: Provenance (retired)
- SLSA v1.1: Provenance (retired)
- in-toto attestation framework
- in-toto attestation: Link predicate v0.3
- SPDX 3.0.1: Build profile
- SPDX 3.0.1: Build class
- GitHub Docs: Using artifact attestations to establish provenance for builds
- GitHub Docs: Variables reference
- Reproducible Builds: SOURCE_DATE_EPOCH
- European Commission: Cyber Resilience Act
Last reviewed: 24 October 2026. Tool center listings and platform plan limits change often; check the linked pages before relying on them.