Command line interface¶
Some of these commands do not exist yet
render, explain, observe, diff, plan, reconcile, status, validate,
affected, agent, config check, revision and version are implemented. init and
migrate are not, and the output shown for those two is illustrative.
Every resource type has a provider. Which one serves a host depends on what it runs, so a type with none here is reported as skipped, which makes a host degraded rather than converged.
Applying is Linux-only. Reading a host works anywhere, since the safety rules the writing path depends on have no portable equivalent.
The commands map onto the pipeline, so each one stops at a different point and prints what it produced.
datum render resolve desired state no host access
datum explain why a resource applies no host access
datum observe read the host read only
datum diff desired against observed read only
datum plan ordered actions read only
datum reconcile apply and verify changes the host
datum status the last pass result no host access
datum agent reconcile on the interval changes the host
Only reconcile changes anything. Everything above it in that list is safe to run on a production
machine in the middle of an incident.
A second group operates on the repository and not on a host, and none of the commands in it read a managed machine at any point.
datum init create a new repository writes local files
datum validate parse and resolve every host no host access
datum affected which hosts a change reaches no host access
datum migrate rewrite documents to a schema writes local files
Two more read and write the agent's own state, not a host or a repository.
datum config check the resolved configuration reads local files
datum revision the accepted revision reads and writes local state
Common flags¶
These are spelled the same way wherever they appear. Each belongs to the commands that take
it rather than to datum itself, so it goes after the command name and not before it.
| Flag | Meaning |
|---|---|
--host NAME |
Resolve for a named host instead of the local one. |
--revision REV |
Use a specific repository revision instead of the current one. |
--repo PATH |
Use a local checkout instead of the configured remote. |
--json |
Emit machine-readable output. |
--version |
Report the version, the revision it was built from and the platform. |
datum --host web-001 render is not accepted, and --version is the one exception, since it
is answered before any command is chosen. On datum affected, --host narrows the report to
one host rather than selecting which host to resolve for.
--host is what makes the read-only commands useful from a laptop. Resolution reads only
repository content, so rendering another host's desired state needs no access to that
machine.
Implementation status
Of the five, --host, --repo and --version are the ones that work. --host is required on
the commands that take it, because nothing works out which host the local machine is, and that is
enrolment, not a missing default. datum agent is the exception,
since it reads host from its configuration and fetches from
source.url instead of needing --repo. --revision and --json are not implemented, so
every command other than the agent reads a checkout named with --repo. datum status has its
own --output json because it is the one command whose structured form is already
designed.
datum render¶
Resolves the effective manifest and prints it.
$ datum render --host web-001
host web-001
revision 8b91f20
manifest sha256:3f2a9c4e
resources 14
Package[curl] base
Package[ca-certificates] base
Package[nginx] roles/web
File[nginx-config] roles/web, hosts/web-001
Service[nginx] roles/web
Sysctl[net.ipv4.ip_forward] base, environments/production
User[www-data] roles/web
Runs entirely against the repository. Two renders of the same revision for the same host produce the same digest, which is what makes the command useful for confirming that a refactor changed nothing.
datum explain¶
Answers why a resource applies to a host, and where each of its field values came from.
$ datum explain File[nginx-config] --host web-001
File[nginx-config] /etc/nginx/nginx.conf
contributed by
roles/web precedence 30 matched role=web
hosts/web-001 precedence 100 matched datum/host=web-001
fields
path /etc/nginx/nginx.conf roles/web
owner root roles/web
group root roles/web
mode 0600 hosts/web-001 overrides 0640 from roles/web
source files/nginx.conf roles/web
requires
Package[nginx] roles/web
This is the command the fleet model exists to make possible, and the requirement that provenance survives composition comes from it and not the other way round.
datum observe¶
Reads and prints the current state of every resource in the manifest, without comparing it to anything.
$ datum observe
File[nginx-config]
exists true
owner root
group root
mode 0644
content sha256:91c4de2a
Service[nginx]
exists true
state running
enabled true
Useful mainly for working out why a diff says what it says, and for confirming that a provider reads a field the way it was expected to.
datum diff¶
Compares desired against observed and prints the differences, without ordering them or deciding what to do.
datum plan¶
Builds the plan and prints it. This is diff plus ordering, dependency handling and
action selection.
$ datum plan
host web-001
revision 8b91f20
manifest sha256:3f2a9c4e
update File[nginx-config]
path /etc/nginx/nginx.conf
provider file
mode 0644 -> 0600
content differs
from roles/web, hosts/web-001
update Service[nginx]
reason File[nginx-config] changed, restartOn matched
none Package[nginx] present, 1.24.0-2
0 to create, 2 to update, 0 to remove, 0 to skip, 12 unchanged
datum reconcile¶
Builds a plan and applies it, then verifies the affected resources.
$ datum reconcile
host web-002
revision d47726f
manifest sha256:8448ab72
mode enforce
converged File[motd] update content
converged File[nginx-config] create /etc/nginx/nginx.conf
skipped Package[nginx] no provider for Package on this host
skipped Service[nginx] no provider for Service on this host
outcome changed
host state degraded
resources 14 total, 5 converged, 0 drifted, 0 failed, 0 blocked, 8 skipped
duration 21ms
Resources with nothing to do are left out, since they are the majority on a healthy host and listing them buries the ones that matter.
A plan is always built fresh from a current observation. There is no flag to apply a plan saved earlier, because a stored plan encodes an observation that has since gone out of date.
Each resource is verified by reading the target again, not by trusting what the provider returned. An action that reported success and did not take is exactly the failure to catch, and a provider cannot be the judge of its own work. A resource that fails blocks everything downstream of it, transitively, so a dependent is never attempted against a prerequisite that is not in place. Unrelated resources are still reconciled, because leaving the rest of a machine unmanaged turns one fault into many.
The pass lock is taken before anything is read, so two passes cannot
observe one host while one of them is partway through changing it. A second invocation exits 3
rather than waiting, because a command that blocks without saying so is indistinguishable from one
that has hung. --wait asks for the other behaviour, which is what a scheduled job wants and not an
operator at a terminal.
| Flag | Meaning |
|---|---|
--mode MODE |
enforce applies the plan, observe reports what differs and changes nothing. |
--state DIR |
Directory for the pass lock and reports. Defaults to /var/lib/datum. |
--wait |
Wait for another pass to finish instead of exiting 3. |
--mode observe runs the same observation and the same diff as enforce, so the drift it
reports is exactly the drift enforce would act on. A pass that found work to do in that
mode ends with the outcome drifted and exits 2.
datum status¶
Reports the result of the last pass on this host.
$ datum status
host web-002
desired
revisionAttempted d47726f
revisionApplied d47726f
manifest sha256:8448ab72
state
condition degraded
mode enforce
last pass
outcome changed
finished 2026-02-08T09:14:22Z (2m14s ago)
duration 21ms
resources
total 14
converged 5
drifted 0
failed 0
blocked 0
skipped 8
skipped Package[nginx] no provider for Package on this host
skipped Service[nginx] no provider for Service on this host
Reads a local report, not the repository or the host, so it is cheap and says nothing about whether the host has drifted since.
| Flag | Meaning |
|---|---|
--state DIR |
Directory the reports were written to. Defaults to /var/lib/datum. |
--output FORMAT |
text or json. |
--resources |
List every resource, including the converged ones. |
The resources printed under the counts are the ones that are not converged, which is the
part somebody reading status during an incident is looking for. --resources prints the whole list
for the cases where the absence of an entry is itself the question.
A host that has never run says so and exits 0, because never having reported is not the
same as having reported a problem.
datum agent¶
Runs as a service, reconciling on the interval in the configuration file.
# datum agent
2026-02-08T09:00:00Z serving metrics on 127.0.0.1:10056
2026-02-08T09:00:00Z host web-001, interval 30m, offset 6m51s, next pass 2026-02-08T09:06:51Z
2026-02-08T09:06:51Z pass converged, 14 of 14 resources converged, next pass 2026-02-08T09:36:51Z
| Flag | Meaning |
|---|---|
--config PATH |
Configuration file. Defaults to /etc/datum/agent.yaml. |
--repo PATH |
Reconcile a local checkout instead of fetching, which verifies nothing. |
--prefix PATH |
Fleet directory within the repository, for a monorepo. |
Each pass clones or fetches source.url, selects a revision according to
trust.require, checks
it descends from the one this host accepted, and reconciles it. A revision that is refused
leaves the host on the last one that worked.
--repo skips all of that, so it is refused unless trust.require is none. That keeps a
host from applying an unverified tree while its own configuration says it should not.
There is no pass at startup. The agent waits for its own offset within the first interval, so a fleet rebooting together does not reconcile at once.
It exits 0 when asked to stop and 1 when it could not start. SIGTERM and SIGINT both
stop scheduling and end a pass that is still running, which leaves the host partially applied
in the way any interrupted pass does.
Running Datum on a host has the unit file.
datum config check¶
Reports the resolved configuration, including the defaults.
# datum config check
/etc/datum/agent.yaml ok
host web-001
source.url https://git.example.com/fleet.git
source.branch main
trust.require none
trust.requireDescendant true
reconciliation.mode enforce
reconciliation.interval 30m, offset 06:51
reconciliation.timeout 15m
metrics.listen 127.0.0.1:10056
state /var/lib/datum root 0700 ok
| Flag | Meaning |
|---|---|
--config PATH |
Configuration file. Defaults to /etc/datum/agent.yaml. |
Printing the resolved values instead of echoing the file is what the command is for, since a key in the wrong place looks correct when a file is read back verbatim. An unrecognised key is reported as a warning after the values, and does not stop the agent.
datum revision¶
Shows or clears the revision this host has accepted.
| Subcommand | Meaning |
|---|---|
show |
Print the accepted revision, or say the host is at first contact. |
clear --yes |
Forget it, so the next signed revision becomes the baseline. |
Clearing is the documented recovery from a history that has been rewritten, where the host cannot tell whether a candidate moves forward or backward and refuses every pass until somebody says which baseline to trust.
It needs --yes because it disarms downgrade protection until the next pass completes. That is a
one-shot action and not a setting, since a control that can be switched off during an incident and
left off is a suggestion.
# datum revision clear --yes
cleared 8b91f2036f4e6b0f5a7c1d2e3f4a5b6c7d8e9f01
the next signed revision this host sees becomes its baseline
datum version¶
Reports what this binary is.
$ datum version
datum 0.1.0~dev
revision 87c303c0399fd34ab377c7dbd827e739eaf822e1, committed 2026-09-23T14:36:12Z
platform linux/amd64, go1.25.5
schema v1alpha1
The schema line lists every schema version this binary reads, newest last. It answers whether a given binary can be pointed at a given repository without having to run it and read the parse error.
datum --version prints the same thing, because a binary is asked its version both ways and
guessing wrong should not be an error.
The revision line is stamped by the Go toolchain from the checkout the binary was built in, so
a build carries its own commit without anything being passed in. A build from a tree with
uncommitted changes says so on that line, and a build with no version passed in reports
unknown instead of a number it was never given. A build from a checkout rather than a release
carries the version 0.1.0~dev, which sorts below any released version.
Verifying a release¶
Each release publishes SHA256SUMS, a detached signature over it as SHA256SUMS.asc, and the
public half of the signing key as signing-key.asc.
$ gpg --import signing-key.asc
$ gpg --verify SHA256SUMS.asc SHA256SUMS
$ sha256sum --check SHA256SUMS
The signature covers the checksum file, which covers every artefact listed in it. Altering an artefact fails the checksum and altering the checksum file fails the signature.
datum init¶
Creates the two documents a repository needs in order to be discovered, and stops there.
$ datum init
created fleet/datum.yaml
created fleet/base/layer.yaml
next steps
add a Host document under fleet/hosts/
add resources under fleet/base/ or a new layer
--with-examples adds a commented example host and resource. The reasoning behind the
the created files being this few, and behind the command leaving Git alone, is set out
under creating a repository.
datum validate¶
Parses every document, checks each against its declared schema version, then resolves and builds a graph for every host in the fleet.
$ datum validate
fleet example
revision 9c02ab
hosts 500
layers 14
schema v1alpha1 (612 documents)
resolved 500 hosts, 0 errors
The schema line counts the documents declaring each version. A repository part-way through a migration lists both, which is where the remaining work shows up.
Errors report how many hosts they affect instead of repeating once per host. Everything
validate reports is an error, so there is nothing to promote and no --strict. There is
no separate lint command either, for the reasons given alongside
it.
datum affected¶
Resolves every host at two revisions and reports which of them end up with a different manifest digest.
$ datum affected --from origin/main --to HEAD
42 of 500 hosts affected
web-001 sha256:3f2a9c4e -> sha256:8d10b7f2
web-002 sha256:3f2a9c4e -> sha256:8d10b7f2
...
db-001 unchanged
--show-resources --host NAME expands one host into a diff between its two
manifests, which reports what that host's desired state becomes, not what
changed in the repository.
datum migrate¶
Rewrites documents from one schema version to another, in place, and leaves the result for a human to review and commit.
$ datum migrate --to v1beta1
rewrote 41 documents in 18 files
fleet/base/packages.yaml
fleet/roles/web/nginx.yaml
...
review the diff before committing
A migration that cannot be performed mechanically stops and names the documents needing a human, and no agent ever performs one. Both points are covered under schema versions.
Exit codes¶
| Code | Meaning |
|---|---|
0 |
Success, and no differences were found by a read-only command. |
1 |
Error. Resolution failed, validation failed, or an action failed. |
2 |
Differences found. From diff, plan, an observe-mode reconcile, and status on a drifted host. |
3 |
Could not acquire the pass lock. Another pass is running. |
Separating code 2 from code 0 is what makes datum diff usable as a drift check in
scheduled jobs. Without it, a script cannot distinguish a converged host from a host with
pending changes, and every caller ends up parsing output.
Open question
Whether reconcile should exit non-zero when resources were skipped is undecided.
Skipping because no provider supports a resource is not a failure of the pass, and a
host quietly sitting at "mostly converged" indefinitely is worse than a noisy exit
code.
Commands deliberately absent¶
There is no command to run something on a host, and no command that takes a shell fragment. That follows from Datum's input being a description of state, and adding one would make every guarantee about planning and verification conditional on whether a repository used it.
There is no datum apply. The name suggests applying a thing that was prepared earlier,
which is exactly the model being avoided, and reconcile names what actually happens.
There is no import or adopt command that turns an existing machine into configuration. Nothing writes to the repository, and generating resources from a host would produce a description of what is there rather than a decision about what should be.