Scripting and CI
The CLI's automation contract in bash and PowerShell: exit codes, JSON and TSV output, discovery, plus a complete CI smoke test.
The CLI is built to live inside scripts: results go to stdout, errors go to stderr as the JSON-RPC code and message, and exit codes follow one contract: 0 success, 1 command or connection error, 2 usage error. Everything on this page is composition on top of that.
Single commands are identical in every shell. swallowtail project info pastes into bash, PowerShell, or cmd unchanged. The tabs below appear where pipelines diverge; PowerShell examples use its native object pipeline (ConvertFrom-Json) rather than transliterated bash.
Machine output
Piped output is pretty-printed JSON. The CLI detects the pipe and never emits color or table layout there, so a script has nothing to strip. (On a terminal, results render as tables and key/value boxes instead; --format json pins exact JSON anywhere.) --format tsv (global flags precede the group) re-renders a result for text tools: an object becomes key<TAB>value rows, an array of objects becomes a header row plus one row per element, and nested values stay as compact JSON. --format ndjson emits one compact JSON value per line when the result is a top-level array, and the whole result on a single line otherwise (most commands wrap arrays in an object, so expect one line there). Unlike TSV, nesting survives intact. The SWALLOWTAIL_FORMAT environment variable pins a format for a whole shell or CI job; the flag always wins, and an unrecognized value warns and falls back to the default.
swallowtail engine commands --group node | jq -r '.methods[]'
swallowtail --format tsv project info | awk -F'\t' '$1 == "main_scene" { print $2 }'(swallowtail engine commands --group node | ConvertFrom-Json).methods
# PowerShell parses JSON natively, so TSV is rarely needed:(swallowtail project info | ConvertFrom-Json).main_sceneDrive every node of a type
Queries compose with actions. batch find-nodes-by-type returns paths that feed straight back into node set:
swallowtail batch find-nodes-by-type --type Label \ | jq -r '.matches[].path' \ | while read -r p; do swallowtail node set --node-path "$p" --property visible --value false done(swallowtail batch find-nodes-by-type --type Label | ConvertFrom-Json).matches.path | ForEach-Object { swallowtail node set --node-path $_ --property visible --value false }Preflight, then launch
swallowtail doctor checks the environment (godot binary, project, addon install state, port config, dotnet) and exits 1 only on a real failure. “No editor running yet” is a warning, so it is safe to run before you have launched anything. swallowtail status gives the machine-readable liveness verdict (running / starting / crashed / closed), and the launch policy follows from it: never start a second editor when one is running. swallowtail status --all scans the editor auto range (9080-9095, plus any pinned port) and the game range (9200-9215), then lists every live instance with its port, project, version, and pid. That is the multi-instance view for when two editors are open at once. swallowtail launch applies that policy for you: it starts one editor for the project, waits until it answers, and returns non-zero if it never does.
The cold half: import, check, test, export, run
Five subcommands spawn the engine directly instead of talking to an editor, which is what a CI job needs before an editor exists. Each writes the engine’s output to <project>/.godot/swallowtail-<name>.log and returns the exact argv it ran, so a failing step is reproducible by hand.
-
swallowtail importbuilds the.godot/import cache a fresh checkout has none of. Run it first. It refuses while an editor is running on the project, because both own that cache; the live-editor form isimport reimport --path res://.... -
swallowtail check . --jobs 4parses every.gdunder the paths you name with--check-onlyand exits1withfile:lineper failure. Directories are walked recursively, skipping.godot/andaddons/unless the path you name is itself inside addons. -
swallowtail testruns the pure-logic tests inres://test/cold, with no editor and no scene tree. A test file is any script that extendsRefCounted, everytest_*method is a test, and the method takes one argument: a harness carryingt.ok,t.eq,t.neandt.fail. It exits1on a failed assertion, on a script error raised inside a test, and on a file that does not parse. -
swallowtail export "<preset>"runs the headless export, writes to the preset’s ownexport_pathunless--outsays otherwise, and reports the file produced, its size, and the engine’s parsed errors. An export that exits 0 having written nothing exits non-zero here, which is what missing export templates look like. -
swallowtail run [scene] --headlessstarts the game standalone with no editor at all. With theswallowtail/runtime/direct_serverproject setting on, it waits for the game’s own channel and reports the port to drive withswallowtail --game.
export, import and test are addon group names as well. A command name after any of them (export list-presets, export info, import reimport, test report) still reaches the running editor.
A complete CI smoke test
#!/usr/bin/env bashset -euo pipefail
swallowtail doctor --project . --json > /dev/null # environment sanity: exit 1 on real problemsswallowtail import # build .godot/ on a fresh checkoutswallowtail check . --jobs 4 # parse every .gd; exit 1 on any failureswallowtail test # run res://test/; exit 1 on any failure
swallowtail launch --headless # one editor, and never a secondEDITOR_PID=$(swallowtail status | jq -r .pid)
swallowtail project info > /dev/null # the editor answersswallowtail scene play --mode mainswallowtail runtime eval --code 'emit(get_tree().current_scene.name)' \ | jq -e '.output[0] == "Main"' # assert against the running gameswallowtail scene stop
kill $EDITOR_PID # export reads the files on disk, so close firstswallowtail export "Windows Desktop" # exit 1 if nothing landed$ErrorActionPreference = "Stop"
swallowtail doctor --project . --json | Out-Null # environment sanity: exit 1 on real problemsif ($LASTEXITCODE) { exit 1 }swallowtail import | Out-Null # build .godot/ on a fresh checkoutif ($LASTEXITCODE) { exit 1 }swallowtail check . --jobs 4 | Out-Null # parse every .gdif ($LASTEXITCODE) { exit 1 }swallowtail test | Out-Null # run res://test/if ($LASTEXITCODE) { exit 1 }
swallowtail launch --headless | Out-Null # one editor, and never a secondif ($LASTEXITCODE) { exit 1 }$editorPid = (swallowtail status | ConvertFrom-Json).pid
swallowtail project info | Out-Null # the editor answersif ($LASTEXITCODE) { exit 1 }swallowtail scene play --mode main | Out-Null$name = (swallowtail runtime eval --code "emit(get_tree().current_scene.name)" | ConvertFrom-Json).output[0]if ($name -ne "Main") { exit 1 } # assert against the running gameswallowtail scene stop | Out-Null
Stop-Process $editorPid # export reads the files on disk, so close firstswallowtail export "Windows Desktop" | Out-Null # exit 1 if nothing landedif ($LASTEXITCODE) { exit 1 }Every step exits non-zero on failure, so set -e (bash) or the explicit $LASTEXITCODE checks (PowerShell) turn any broken link into a red build.
The catalog is data
engine commands returns the whole tool surface as JSON: the method list, a group map, and per-command param docs (attached for --group, or --docs for the full catalog). Generators become trivial: bindings, wrappers, forms, docs that cannot drift.
# every node command with its required flagsswallowtail engine commands --group node \ | jq -r '.docs | to_entries[] | .key + " " + ([.value.params[]? | select(.required) | "--" + .name] | join(" "))'# every node command with its required flags$cat = swallowtail engine commands --group node | ConvertFrom-Json$cat.docs.PSObject.Properties | ForEach-Object { $_.Name + " " + ((@($_.Value.params) | Where-Object required | ForEach-Object { "--" + $_.name }) -join " ")}Standalone game rigs
With the direct-to-player channel enabled, a QA rig needs no editor at all: run a debug build, then swallowtail --game runtime tree, --game runtime eval …, and --game input … drive it directly. See Playtest loop.
Structured command errors
For editor commands, --errors json emits one JSON error object on stderr while
leaving stdout empty on failure. The envelope is { "error": { "kind": "command", "message": "...", "data": { "code": -32601 } } }; RPC data remains available.
Usage and connection failures use kind: "usage" and kind: "connection".
This option applies to editor-command execution, not local subcommands or help.
swallowtail --errors json scene validate --path res://hello.tscnDefault typo errors suggest nearby commands instead of printing the entire catalog.
Global flags precede the group. If an addon rejects a misplaced --format,
--port, or --timeout, the error explains the correct location. Addon parameters
with those names retain their meaning after the command.
Inspection in a terminal
Pretty scene and runtime trees use branches. Scene and single-script validation
show a verdict and diagnostic locations. COLUMNS=40 controls plain-output
wrapping; long identifiers stay intact so file locations can be copied.
Use --format json for complete structured data in scripts.
Shell completion and offline help
Generate a completion script once, then source it from your shell profile.
swallowtail completion bash > swallowtail-completion.bashsource ./swallowtail-completion.bashswallowtail completion powershell | Set-Content -Encoding utf8 swallowtail-completion.ps1. ./swallowtail-completion.ps1Completion suggests local commands, live addon groups and commands, and parameter names. It does not complete resource paths or parameter values. Bash completion requires Bash 4 or newer; PowerShell completion uses its native argument completer.
Run help from inside a project while its editor is available to save its catalog:
swallowtail scene --helpWhen that editor is unavailable, help can use the saved catalog in the project’s
.godot/ directory. A timestamped notice identifies cached help. Completion can
also use this cache. An uncached project still needs a live editor for addon help;
local command help works without one. Live lookups refresh custom commands after
an editor restart. Cached documentation never makes an unavailable command executable.
Select a project from another directory
Use the global --project before the group to target a project without changing
your shell directory:
swallowtail --project "/games/My Game" scene treeswallowtail --project "/games/My Game" scene --helpswallowtail --project "/games/My Game" doctorThis uses that project’s discovery file. An explicit port that answers for a
different project is refused before an editor command runs. Local subcommands
continue to accept their existing --project flag after the subcommand; if both
forms are present, the local flag wins. create uses its own --path instead.
Relative input file paths remain relative to your shell directory.
Read parameters from JSON
For larger property maps or scenarios, save the parameter object in a UTF-8 file.
For example, inspect.json can contain:
{"node_path":"Greeting","properties":["text","position"]}swallowtail --project "/games/My Game" --params-file inspect.json node getcat inspect.json | swallowtail --project "/games/My Game" --params-file - node getGet-Content -Raw -Encoding utf8 inspect.json | swallowtail --project "C:/games/My Game" --params-file - node getInline parameters override matching file keys. Kebab-case keys normalize to
snake-case; conflicting spellings in the same file are rejected. Input must be
one JSON object, at most 16 MiB. UTF-8 BOMs are accepted. Arrays, null, malformed
JSON, and unreadable files fail before contacting the editor. A literal @ in an
inline value stays literal. File input is available for editor/game commands,
not local subcommands or MCP envelopes.
Validation results can report valid: false in a successful response: check that
field when using live validation as an automation gate. The local check and
test subcommands already return nonzero when their checks fail.
Run a command plan
Use swallowtail automate to run editor commands in sequence and check their results.
Save a plan as plan.json:
{ "steps": [ {"method": "engine.version", "expect": {"/version/major": 4}}, {"method": "scene.validate", "expect": {"/valid": true}}, {"method": "scene.save"} ]}swallowtail automate --project "/games/My Game" --file plan.json --dry-runswallowtail automate --project "/games/My Game" --file plan.json --timeout 60sThe runner checks the entire plan's structure and method availability before
executing its steps. It also confirms that the answering editor serves your
target project, including when you supply --port. Dry run checks structure and
method availability; it does not validate parameters, scene state, or assertions.
The editor still validates each command's parameters when that step runs.
Each step takes method and optional params, expect, assert, capture, and
wait_until. Expectation keys are JSON pointers into the result, such as /valid
or /nodes/0/name. Values must match exactly. A missing field fails even when
the expected value is null. An empty pointer checks the whole result.
Output is one JSON report with each step's index, method, duration, result or
error, and status: passed, failed, skipped, or validated for a dry run.
Exit code 0 means all checks passed, 1 means execution or preflight failed, and
2 means the input or configuration was invalid. Setup failures go to stderr;
use the global --errors json for structured errors there.
The runner stops on the first failure. --continue-on-error allows later steps
after command or assertion failures; connection loss, timeouts, and cancellation
always stop execution. Completed changes remain in place, and nothing is rolled
back. Only wait_until repeats a call. --timeout applies to each call, and
Ctrl+C cancels the current request and stops later steps. Plans accept UTF-8 JSON
with an optional BOM, up to 4 MiB and 1,000 steps. Use --file - to read stdin.
Retained results are limited to 64 MiB; save large captures to files through
their command options.
Capture values and assert on them
This plan reads a label's text in the editor, plays the scene, and waits until the running game shows the same text:
{ "steps": [ {"method": "scene.open", "params": {"path": "res://main.tscn"}}, {"method": "node.get", "params": {"node_path": "Label", "property": "text"}, "capture": {"title": "/properties/text"}, "assert": [{"path": "/properties/text", "op": "contains", "value": "Godot"}]}, {"method": "scene.play", "params": {"mode": "current"}}, {"method": "runtime.get", "params": {"node_path": "Label", "properties": ["text"]}, "wait_until": {"timeout_ms": 30000, "interval_ms": 250}, "assert": [{"path": "/properties/text", "op": "eq", "value": "{{title}}"}]}, {"method": "scene.stop"} ]}capture maps a name to a JSON pointer and stores that value once the step
passes. Later params and assertion values use it as {{title}}. A string that
is exactly {{title}} takes the captured value with its JSON type, so a number
stays a number; inside longer text the value is spliced in. A name must be
captured by an earlier step, or the plan is refused before anything runs. Under
--continue-on-error, a step whose variable was never captured is skipped with
the reason and not called.
assert is a list of checks, each {"path", "op", "value"}:
| op | Passes when the value at path |
|---|---|
eq, neq | equals, or differs from, value |
gt, gte, lt, lte | compares to value as a number |
contains | holds value as a substring, or as an array element |
exists | is present (value: true) or absent (value: false) |
Add "tolerance": 0.01 to eq or neq to compare numbers within a margin. A
number comparison against a string or any other non-number fails the check and
says so. A failed step lists every failed check with its path, the expected
value, and the actual one. expect keeps working as the short form of eq.
Wait for a condition
wait_until repeats a step until its expect and assert checks pass.
timeout_ms is 1 to 600000, and interval_ms is 50 to 60000, 500 by default.
Command errors are retried, apart from an unknown method or invalid params. A
timeout fails the step as an assertion, and the report records the attempt
count and the last failure. --timeout still bounds each call.
Write reports for CI
swallowtail automate --project . --file plan.json --report report.json --junit junit.xml--report writes the same JSON document that goes to stdout. --junit writes
JUnit XML with one test case per step: a failed check is a <failure>, and a
command, connection, or preflight error is an <error>. Both files are written
even when the run fails, including when the editor cannot be reached and stdout
stays empty, so a CI job can upload them in every case. The exit code does not
change.
Handle an uncertain result
With --errors json, transport failures distinguish a connection that could not
send the request (outcome: "not_sent") from a lost response after delivery was
attempted (outcome: "unknown"). Timeouts and cancellation have their own error
kinds. A timeout can happen after a mutation has completed, so inspect the scene,
script, or exported file before repeating that operation. Cooperative commands
stop at their next cancellation boundary; completed effects remain.