Reference
Everything on this page is lookup material. If you're learning the tool, start with the Quickstart and How It Works instead.
Commands
| Command | What it does |
|---|---|
d6s profile add | Add or update a profile (name + URL) and optionally save a credential |
d6s profile test | Connect with a profile and print who you are on the instance |
d6s sync pull | Write a source instance's schema and configuration to committable files |
d6s sync diff | Show what a push would change on the target; applies nothing |
d6s sync push | Apply the committed files to a target instance |
d6s sync | Interactive wizard: prompts for source, target, project, and mode, then pulls and pushes |
The CLI installs as directus-cli with d6s as an equivalent short alias.
Global flags, available on every command:
| Flag | Effect |
|---|---|
--json | One machine-readable report on stdout; human status stays off stdout |
--no-color | Disable colored output |
--no-interactive | Disable prompts; behave as in CI |
--config <path> | Path to directus.config.json (default: found by walking up from the current directory) |
d6s profile add
d6s profile add [name] [--url <url>] [--token <token>] [--yes]
| Flag | Effect |
|---|---|
--url <url> | Directus instance URL |
--token <token> | Static token to save to the credential store for this profile |
--yes | Skip the confirmation when repointing an existing profile to a new URL |
Adding is an upsert: an existing name is updated. Run without arguments for prompts, which also offer to save a credential (paste a static token, or log in with email and password to save a session; saved sessions refresh themselves before they expire). Profile names use letters, numbers, and underscores. URLs with embedded credentials, query strings, or fragments are refused, because the URL is the part that lands in a committed file.
d6s profile test
d6s profile test <name>
| Flag | Effect |
|---|---|
--url <url> | Test a URL directly, without a profile or config file |
--token <token> | Override the resolved token |
Connects and prints who the credential authenticates as:
◇ Authenticated to https://cms.example.com as Admin User (Administrator).
d6s sync pull
d6s sync pull --from <profile> [scope flags]
| Flag | Effect |
|---|---|
--from <profile> (required) | Source profile name |
--collections <list> | Schema scope: only these collections (comma-separated) |
--exclude-collections <list> | Schema scope: all collections except these |
--no-schema | Skip the schema entirely; configuration resources only |
--<resource> | Select only the named resources, e.g. --flows --roles (plus their dependencies) |
--no-<resource> | Keep the default resource set but exclude one, e.g. --no-flows |
--all | Every configuration resource, including users and translations |
--no-deps | Do not pull a selected resource's dependencies (dependent children still ride with their parent) |
--project <name> | Project to sync (default: default) |
The selectable resources are roles, policies, flows, dashboards, settings, folders, users, and translations; access, permissions, operations, and panels ride along with their parents (see the dependency table). Positive selection (--flows) cannot be combined with --all or with --no- flags. Resource selection never narrows the schema; the two axes are scoped independently.
Two warnings a scoped pull can raise, neither of which widens the scope for you:
- Out-of-scope references. The scoped snapshot points at something you omitted (a relation target, a group parent, a many-to-any collection). Pushing that snapshot to a fresh target can fail; add the missing collections to
--collectionsyourself. - A name the server didn't return. A
--collectionsname absent from the returned snapshot (usually a typo) is named in a warning. The partial snapshot still lands, but never silently.
d6s sync diff
d6s sync diff --to <profile> [--mode <mode>] [--allow-version-drift]
| Flag | Effect |
|---|---|
--to <profile> (required) | Target profile name |
--mode <mode> | add, merge, or mirror; changes what the preview plans for |
--allow-version-drift | Preview despite a version mismatch (see the version rule) |
--project <name> | Project to sync (default: default) |
Applies nothing, and exits 0 whether or not differences exist; automation reads the report's changes field.
d6s sync push
d6s sync push --to <profile> [--mode <mode>] [--yes] [--dangerously-allow-delete] [--allow-version-drift]
| Flag | Effect |
|---|---|
--to <profile> (required) | Target profile name |
--mode <mode> | add, merge (default), or mirror |
--yes | Skip the apply confirmation; never authorizes deletions |
--dangerously-allow-delete | Consent to deletions; required for non-interactive mirror |
--allow-version-drift | Push despite a version mismatch (see the version rule) |
--project <name> | Project to sync (default: default) |
directus.config.json
Created and updated by d6s profile add; found by walking up from the current directory, like git finds .git. It never contains credentials, so commit it. The full shape:
{
"profiles": {
"staging": { "url": "https://staging.example.com" },
"production": { "url": "https://cms.example.com" }
},
"directory": "directus",
"projects": {
"default": {
"schema": true,
"collections": ["pages", "posts"],
"resources": ["flows", "settings"],
"mode": "merge"
}
}
}
Top-level keys:
| Key | Meaning | Default |
|---|---|---|
profiles | Named instances: { "url": "https://..." } per profile | {} |
directory | The directory pulls write into and pushes read from | "directus" |
projects | Per-project sync options (see below) | {} |
Per-project keys, all optional. A project is a named slice of the sync with its own subdirectory (<directory>/<project>/); the default project exists without being declared. Flags on the command line override these per run:
| Key | Meaning |
|---|---|
schema | false makes this a configuration-only project: pull, diff, and push never touch schema, and reports say schemaSkipped so automation can tell a skipped phase from a matching one |
collections | Schema scope: only these collections |
excludeCollections | Schema scope: all collections except these |
resources | Only these configuration resources |
excludeResources | The default resources except these |
mode | Default push mode for this project: add, merge, or mirror |
deps | false skips pulling selected resources' dependencies |
"schema": false combined with a collections scope is refused as a contradiction, as is setting both an include and an exclude list for the same axis. Project names use letters, numbers, hyphens, and underscores.
Credentials
When a command authenticates a profile, the token resolves in order:
- A
--tokenflag, on the twoprofilecommands that accept one. The sync commands take no token flag. - The
DIRECTUS_<PROFILE>_TOKENenvironment variable: the profile name uppercased, soproductionreadsDIRECTUS_PRODUCTION_TOKEN. A.envfile next todirectus.config.jsonis loaded automatically without overriding real environment variables. - The credential store at
~/.directus/credentials.json, written readable only by you (mode0600). Never consulted when theCIenvironment variable is set.
Use an admin credential. The schema endpoints and the batch import the CLI relies on are admin-only on the server; the CLI does not check privileges up front, so a non-admin token fails with an authentication error, or produces an incomplete export that the completeness checks then flag.
Configuration resources
Resource selection follows a dependency graph: selecting a resource pulls in what it needs (unless --no-deps).
| Resource | In default pull | Select directly | Pulls in | Notes |
|---|---|---|---|---|
roles | Yes | --roles | policies | |
policies | Yes | --policies | access, permissions | See the warning below before selecting policies on their own. |
access | Yes, with roles and policies | — | — | Grants attached to users are dropped when users are out of scope; a mirror push does not delete them on the target. |
permissions | Yes, with policies | — | — | Row counts are verified against the server. If the source hides rows (unlicensed custom permission rules), the export is marked incomplete. |
flows | Yes | --flows | operations | |
operations | Yes, with flows | — | — | |
dashboards | Yes | --dashboards | panels | |
panels | Yes, with dashboards | — | — | Panels have no field to match on, so a first push into a matching target can duplicate once; the identity map prevents repeats. |
settings | Yes | --settings | — | A single record. License and AI credentials, branding images (logos, backgrounds, favicon), and the default storage folder are stripped. |
folders | Yes | --folders | — | The media-library folder tree. Distinct from collection folders (Data Studio sidebar groups), which sync as schema. |
users | Opt-in | --users | roles, policies | Secret columns (password, token, tfa_secret, and others) are stripped. |
translations | Opt-in | --translations | — | Mirror pushes of translations are not currently supported, which is why they are opt-in. |
--roles, not --policies alone
A selection that pulls policies without roles is not independently pushable when access rows reference roles: access rows carry references to roles, and with no roles in scope a push to a fresh target fails. Select --roles instead; it pulls policies and their children too.What a pull touches
Two rules govern every pull, scoped or not:
- A pull only rewrites what it fetched. Everything it did not fetch keeps its committed state, untouched.
- A push only applies what is in the files. Work that never entered them cannot ship.
Refreshed means the file is rewritten from the source; because writes are deterministic, an unchanged resource produces no git diff. Preserved means the file is not touched at all.
| Pull | Schema files | Configuration files |
|---|---|---|
pull --from staging | All refreshed | All refreshed |
... --collections posts | posts refreshed, others preserved | All refreshed |
... --no-flows | All refreshed | Flows preserved, others refreshed |
... --flows | All refreshed (resource selection never narrows schema) | Flows and operations refreshed, others preserved |
... --flows --no-schema | All preserved | Flows and operations refreshed, others preserved |
... --collections posts --no-flows | posts refreshed, others preserved | Flows preserved, others refreshed |
Push modes
| Mode | Schema | Data | Deletes? |
|---|---|---|---|
add | Adds and modifies, same as merge | Inserts only; existing records are never updated | No |
merge (default) | Adds and modifies | Creates and updates | No |
mirror | May delete in scope | Creates, updates, and deletes records absent from the files | Yes, gated |
A mirror push deletes only within what the committed files cover: a snapshot scoped to some collections can delete fields inside those collections, never a collection it doesn't contain. An export the source itself left incomplete (hidden permission rows) is refused at mirror push outright.
Deletion gates
Only mirror deletes, and deleting always requires its own explicit consent:
| Context | To delete you must |
|---|---|
| Interactive terminal | Review the plan naming the losses, then type the profile name (unless you passed --dangerously-allow-delete, which is the consent) |
| Non-interactive / CI | Pass --dangerously-allow-delete |
--yes skips the ordinary confirmation prompt, but it never authorizes a deletion; that holds even if a supposedly additive push unexpectedly carries one. A mirror push in CI without --dangerously-allow-delete refuses before changing anything:
✖ Refusing mirror mode in a non-interactive context without --dangerously-allow-delete.
mirror can delete schema and data rows absent from the import set; pass --dangerously-allow-delete to consent, or use --mode merge.
Record identity
Records are matched across instances first by the committed identity map (<directory>/<project>/id_map.json), then by an identifying field:
| Resource | Matched by |
|---|---|
| Most resources | name |
| Users | email |
| Operations | key |
| Panels | Nothing; identity map only |
The map is keyed internally by source and target instance URL, so one committed file serves any number of targets without conflicts. Commit it whenever a push changes it. An ambiguous match (two candidates) prompts interactively and refuses non-interactively:
✖ Ambiguous target matches:
directus_roles source "Editor" — sr1 → one of t1, t2
Run d6s sync push interactively once to choose, then commit the updated id map.
The version rule
Schema changes require the snapshot's Directus version and the target's version to match exactly, patch release included; the mismatch error names both versions. --allow-version-drift proceeds anyway with a warning; the CLI never translates schema between versions. Projects with "schema": false skip the check entirely, and a target whose version cannot be read is left to the server's own gate rather than refused.
JSON reports
With --json, stdout carries exactly one report per command; warnings still go to stderr so logs keep them while stdout stays parseable. Reports are emitted as a single line; they're formatted here for readability.
d6s sync pull --from staging --json:
{
"kind": "PullReport",
"formatVersion": 1,
"ok": true,
"source": "https://staging.example.com",
"profile": "staging",
"project": "default",
"schemaSkipped": false,
"dir": "directus/default/schema",
"collections": 12,
"fields": 87,
"systemFields": 2,
"relations": 14,
"files": 13,
"removed": [],
"scope": null,
"data": {
"resources": ["directus_flows", "directus_roles"],
"collections": 10,
"records": 57,
"files": 10,
"removed": [],
"incomplete": []
}
}
The schema counters (collections through files) are null when the schema phase is skipped; scope echoes a --collections/--exclude-collections scope; data.incomplete names resources whose export the source cut short.
d6s sync diff --to production --json:
{
"kind": "DiffReport",
"formatVersion": 1,
"ok": true,
"target": "https://cms.example.com",
"profile": "production",
"project": "default",
"mode": "merge",
"changes": true,
"unresolved": 0,
"schemaSkipped": false,
"added": 1,
"modified": 1,
"deleted": 0,
"data": {
"mode": "merge",
"source": "https://staging.example.com",
"collections": {
"directus_flows": {
"existing": [],
"new": ["f1"],
"deleted": [],
"mapped": {}
}
},
"matched": 1,
"ambiguous": 0,
"unmatched": 1,
"unchanged": 0,
"incomplete": [],
"skipped": false
}
}
changes is true when a push would do anything, including when records are unresolved; added/modified/deleted count schema items; data.collections is the target server's own per-collection dry-run answer.
d6s sync push --to production --yes --json reports the same shape as a diff, with applied (true when the push changed the target) in place of unresolved, and data.collections reflecting what the import actually did.
Failures put an error report on stdout:
{
"kind": "ErrorReport",
"formatVersion": 1,
"error": {
"code": "STATE",
"message": "Version mismatch: the snapshot was pulled from Directus 11.2.0, but the target runs 11.2.5.",
"hint": "..."
}
}
The code is one of a small set of failure classes: USAGE (the command line needs fixing: a missing flag or missing consent), CONFIG (directus.config.json missing or invalid), AUTH (the credential was rejected), HTTP (the instance could not be reached or returned an error), STATE (the committed files and the instance disagree: version mismatch, changed target schema, incomplete export), or UNKNOWN. Exit codes are 0 for success and 1 for every failure; the code string is the finer-grained signal.
Output conventions
Human-readable status lines go to stderr, prefixed ● (info), ◇ (success), ▲ (warning), or ✖ (error, with its hint indented beneath). Plan lines go to stdout: + marks an addition, ~ a modification, and ✖ DELETE a deletion, with data plans summarized per collection as +N new ~N updated ✖N deleted. --no-color disables coloring; --json replaces stdout output with the report while warnings stay on stderr.
Get once-a-month release notes & real‑world code tips...no fluff. 🐰