Headless mode
Section titled “Headless mode”Run Antigravity CLI non-interactively to script agent tasks, integrate with CI pipelines, and capture machine-readable output.
Headless mode (also called print mode) sends a single prompt to the agent, streams or returns the response, and exits. Use it whenever you need the agent’s output in a program instead of a terminal UI.
Run a single prompt
Section titled “Run a single prompt”Pass a prompt with -p (or its aliases --print and --prompt) to run once and exit:
agy -p "In one sentence, what is a git rebase?"
A git rebase rewrites the commit history by transplanting a sequence of commits onto a new base commit, imposing a strictly linear progression of changes that eliminates arbitrary merge artifacts.
The response goes to stdout. Diagnostics — errors, authentication prompts, progress, and permission notices — go to stderr. This split keeps the captured response clean:
# Capture only the model response; diagnostics still print to the terminal.
answer=$(agy -p "Name three popular version control systems, comma-separated.")
Note: Headless mode uses your cached credentials. Authenticate once with an interactive
agysession first. In a non-interactive environment with no terminal (for example, CI), a run that is not already authenticated exits with anauthentication requirederror instead of hanging.
Output formats
Section titled “Output formats”The --output-format flag controls the shape of stdout. It accepts three values:
| Format | stdout shape | Use it for |
|---|---|---|
text | The response text (default) | Human-readable output, quick scripts |
json | One JSON object printed on completion | Capturing a result plus metadata |
stream-json | Newline-delimited JSON (NDJSON) events | Monitoring progress, tools, and token usage |
The default. The response text goes straight to stdout with no wrapping:
agy -p "In one sentence, what does the command git bisect do?"
Git bisect executes a binary search algorithm across a project's commit history to rapidly isolate the precise commit responsible for introducing a defect.
Set --output-format json to get a single JSON envelope after the run completes. The CLI emits it on one line; pipe through jq to pretty-print:
agy -p "In one sentence, what is a git rebase?" --output-format json | jq
{
"conversation_id": "055a398f-db14-4c5f-abbb-1bf03f8120a7",
"status": "SUCCESS",
"response": "A git rebase rewrites the commit history by transplanting a sequence of commits onto a new base commit, imposing a strictly linear progression of changes that eliminates arbitrary merge artifacts.\n",
"duration_seconds": 7.16,
"num_turns": 1,
"usage": {
"input_tokens": 10415,
"output_tokens": 657,
"thinking_tokens": 616,
"cache_read_tokens": 8113,
"total_tokens": 11072
}
}
The envelope contains these fields:
| Field | Type | Description |
|---|---|---|
conversation_id | string | ID of the conversation, for resuming later |
status | string | Terminal status (see Status values) |
response | string | The agent’s free-text response |
error | string | Error message; present only on failure |
duration_seconds | number | Wall-clock duration of the run |
num_turns | number | Number of user turns in the conversation |
structured_output | object | Parsed schema output; present only with --json-schema |
json_schema | object | The schema that was enforced; present only with --json-schema |
usage | object | Token counts: input_tokens, output_tokens, thinking_tokens, cache_read_tokens, total_tokens |
Structured output with a schema
Section titled “Structured output with a schema”Pass --json-schema to constrain the answer to a schema. The parsed object appears in structured_output, and response holds the same payload serialized as a string:
agy -p "Parse the semantic version string v2.14.3 into an object with integer fields major, minor, and patch." \
--output-format json \
--json-schema '{"type":"object","properties":{"major":{"type":"integer"},"minor":{"type":"integer"},"patch":{"type":"integer"}},"required":["major","minor","patch"]}' | jq
{
"conversation_id": "4e502687-290c-4030-b908-5ed6c68fa5dc",
"status": "SUCCESS",
"response": "{\"major\":2,\"minor\":14,\"patch\":3}\n",
"duration_seconds": 4.45,
"num_turns": 1,
"structured_output": { "major": 2, "minor": 14, "patch": 3 },
"json_schema": {
"type": "object",
"properties": {
"major": { "type": "integer" },
"minor": { "type": "integer" },
"patch": { "type": "integer" }
},
"required": ["major", "minor", "patch"]
},
"usage": { "input_tokens": 10522, "output_tokens": 354, "thinking_tokens": 329, "cache_read_tokens": 8112, "total_tokens": 10876 }
}
The flag accepts a schema string, a path to a .json schema file, or a primitive type name (string, number, integer, boolean). Read the parsed value from structured_output:
agy -p "Parse the semantic version string v2.14.3 into an object with integer fields major, minor, and patch." \
--output-format json \
--json-schema '{"type":"object","properties":{"major":{"type":"integer"},"minor":{"type":"integer"},"patch":{"type":"integer"}},"required":["major","minor","patch"]}' \
| jq '.structured_output'
Streaming JSON
Section titled “Streaming JSON”Set --output-format stream-json to emit one JSON object per line (NDJSON) as the run progresses. Use this format to observe tool calls and token usage in real time.
agy -p "In one sentence, what is a git rebase?" --output-format stream-json
The stream begins with one init event, followed by any number of step_update events, and ends with exactly one result event (the cwd and tools array are abbreviated below):
{"event":"init","conversation_id":"c3b66b04-872b-4fbe-a3a4-058a026ef20a","init":{"cwd":"/home/user/project","tools":["ask_permission","run_command","write_to_file","..."],"permission_mode":"request-review"}}
{"event":"step_update","step_update":{"conversation_id":"c3b66b04-872b-4fbe-a3a4-058a026ef20a","step_index":0,"state":"DONE","step_type":"user_input"}}
{"event":"step_update","step_update":{"conversation_id":"c3b66b04-872b-4fbe-a3a4-058a026ef20a","step_index":3,"state":"DONE","step_type":"agent_response","text_delta":"Git rebase destructively rewrites a branch's commit history by systematically detaching its unique commits and sequentially reapplying them onto a new base commit.\n","duration_seconds":6.28,"usage":{"input_tokens":10302,"output_tokens":582,"thinking_tokens":551,"cache_read_tokens":8113,"total_tokens":10884}}}
{"event":"step_update","step_update":{"conversation_id":"c3b66b04-872b-4fbe-a3a4-058a026ef20a","step_index":4,"state":"DONE","step_type":"checkpoint","duration_seconds":0.53,"usage":{"input_tokens":116,"output_tokens":7,"thinking_tokens":0,"cache_read_tokens":0,"total_tokens":123}}}
{"event":"result","result":{"conversation_id":"c3b66b04-872b-4fbe-a3a4-058a026ef20a","status":"SUCCESS","response":"Git rebase destructively rewrites a branch's commit history by systematically detaching its unique commits and sequentially reapplying them onto a new base commit.\n","duration_seconds":6.88,"num_turns":1,"usage":{"input_tokens":10418,"output_tokens":589,"thinking_tokens":551,"cache_read_tokens":8113,"total_tokens":11007}}}
When the response streams in chunks, the agent_response step emits one or more ACTIVE events carrying partial text_delta fragments before its final DONE; short responses like this one arrive in a single DONE.
Every line is an event object whose event field names its type:
event | Payload key | Emitted |
|---|---|---|
init | init | Once, at stream start |
step_update | step_update | For each step transition or text delta |
result | result | Once, at the end (same shape as json) |
The init payload records the run configuration. model and agent appear only when set with --model or --agent; permission_mode is request-review by default (and always-proceed under --dangerously-skip-permissions):
| Field | Type | Description |
|---|---|---|
cwd | string | Working directory |
tools | string[] | Names of all available tools |
permission_mode | string | Effective permission mode |
model | string | Model in use, when overridden |
agent | string | Active agent, when overridden |
json_schema | object | Enforced schema, when set with --json-schema |
Each step_update payload describes one step. Observed step_type values include user_input, agent_response, tool, and checkpoint; state is ACTIVE while a step runs and DONE when it finishes:
| Field | Type | Description |
|---|---|---|
conversation_id | string | ID of the conversation |
step_index | number | Zero-based index of the step |
state | string | ACTIVE or DONE |
step_type | string | Step category, for example agent_response or tool |
tool_name | string | Canonical tool name, on tool steps |
text_delta | string | Incremental response text |
duration_seconds | number | Step duration, when known |
usage | object | Per-step token usage, when known |
tool_info | object | Tool invocation details (see below) |
subagent_info | object | Subagent invocation details |
Tool calls in the stream
Section titled “Tool calls in the stream”On tool steps, tool_info carries the call and its result. This is a real tool step from a run that executed echo hello_headless_demo:
{"event":"step_update","step_update":{"conversation_id":"edb1c8c1-50ba-4f3f-87eb-412d0e9d47c3","step_index":4,"state":"DONE","step_type":"tool","tool_name":"run_command","duration_seconds":0.07,"tool_info":{"name":"run_command","parameters":{"CommandLine":"echo hello_headless_demo"},"output":"hello_headless_demo\r\n"}}}
tool_info holds name, parameters, output, and — when the tool fails — an error object with type and message. Steps that spawn subagents carry subagent_info instead, listing each subagent under subagents (with type_name, role, conversation_id, log_uri, and workspace_uris).
Structured output in the stream
Section titled “Structured output in the stream”With --json-schema, the schema applies to the terminal result event, which carries the same structured_output and json_schema fields as the json envelope.
Parse output with jq
Section titled “Parse output with jq”stdout is machine-readable, so jq extracts exactly what you need.
Get the response text from a JSON run:
agy -p "Name three popular version control systems, comma-separated." --output-format json | jq -r '.response'
Git, Subversion, Mercurial.
Concatenate streaming text as it arrives:
agy -p "Explain what a merge conflict is in two sentences." --output-format stream-json \
| jq -j 'select(.event=="step_update") | .step_update.text_delta // empty'
Read token usage from the terminal result event:
agy -p "In one sentence, what is a git rebase?" --output-format stream-json \
| jq 'select(.event=="result") | .result.usage'
Tip: Use
jq -j(join output) when concatenatingtext_deltafragments sojqdoes not insert newlines between them.
Continue a conversation
Section titled “Continue a conversation”Headless runs are stateless by default. Resume prior context with --continue (-c) for the most recent conversation, or --conversation with an ID from a previous run’s conversation_id:
# Continue the most recent conversation.
agy -p "Now explain your previous answer in more detail" --continue
# Resume a specific conversation by ID.
agy -p "Summarize what we discussed" --conversation 055a398f-db14-4c5f-abbb-1bf03f8120a7
Each of these starts a new process. To run multiple turns inside one process, see Stream prompts from stdin.
Stream prompts from stdin
Section titled “Stream prompts from stdin”Use --input-format stream-json to maintain a single, continuous conversation process, feeding it prompts one by one on standard input (stdin). Each prompt executes a full turn and emits its own result event.
This approach is ideal for applications that dynamically determine the next prompt based on the previous answer. Because the process only starts once, subsequent turns skip startup overhead and reuse the warmed-up conversation. This makes it significantly faster than running repeated commands with --continue.
Note:
--input-format stream-jsonrequires--output-format stream-json. In a streaming session, the CLI emits exactly oneresultevent per turn.
Send a prompt
Section titled “Send a prompt”Write one JSON object per line to stdin. The event key specifies the message type (matching the output stream format). A prompt is represented as a user event with a message:
{ "event": "user", "message": { "content": "Reply with exactly the word: apple. Nothing else." } }
You can pipe multiple prompts into a single session:
printf '%s\n' \
'{"event":"user","message":{"content":"Reply with exactly the word: apple. Nothing else."}}' \
'{"event":"user","message":{"content":"What word did I ask you to reply with in my previous message? Answer with just that word."}}' \
| agy --input-format stream-json --output-format stream-json
The content field accepts either a standard string or a list of text blocks. The following two formats are equivalent:
{ "event": "user", "message": { "content": "Reply with exactly: banana" } }
{ "event": "user", "message": { "content": [{ "type": "text", "text": "Reply with exactly: banana" }] } }
text is the only supported block type. Submitting any other block type ends the session with an error message rather than silently dropping the block. This ensures the agent never answers a prompt you didn’t explicitly send.
Read the results
Section titled “Read the results”The output stream works as follows:
-
Opens with a single
initevent. -
Emits a series of
step_updateevents for the active turn. -
Concludes the turn with a final
resultevent.
This example shows the output from the two-prompt bash command above (with the init payload abbreviated):
{"event":"init","conversation_id":"9ec58bfd-4d67-4f5e-83a5-9d907e9c6b1f","init":{"cwd":"/home/user/project","tools":["ask_permission","run_command","write_to_file","..."],"permission_mode":"request-review"}}
{"event":"step_update","step_update":{"conversation_id":"9ec58bfd-4d67-4f5e-83a5-9d907e9c6b1f","step_index":0,"state":"DONE","step_type":"user_input"}}
{"event":"step_update","step_update":{"conversation_id":"9ec58bfd-4d67-4f5e-83a5-9d907e9c6b1f","step_index":2,"state":"ACTIVE","step_type":"agent_response","text_delta":"apple"}}
{"event":"step_update","step_update":{"conversation_id":"9ec58bfd-4d67-4f5e-83a5-9d907e9c6b1f","step_index":2,"state":"DONE","step_type":"agent_response","text_delta":"\n","duration_seconds":1.169607627,"usage":{"input_tokens":30384,"output_tokens":4,"thinking_tokens":0,"cache_read_tokens":0,"total_tokens":30388}}}
{"event":"result","result":{"conversation_id":"9ec58bfd-4d67-4f5e-83a5-9d907e9c6b1f","status":"SUCCESS","response":"apple\n","duration_seconds":1.427806958,"num_turns":1,"usage":{"input_tokens":30384,"output_tokens":4,"thinking_tokens":0,"cache_read_tokens":0,"total_tokens":30388}}}
{"event":"step_update","step_update":{"conversation_id":"9ec58bfd-4d67-4f5e-83a5-9d907e9c6b1f","step_index":3,"state":"DONE","step_type":"user_input"}}
{"event":"step_update","step_update":{"conversation_id":"9ec58bfd-4d67-4f5e-83a5-9d907e9c6b1f","step_index":4,"state":"DONE","step_type":"agent_response","text_delta":"apple\n","duration_seconds":0.895679386,"usage":{"input_tokens":278,"output_tokens":4,"thinking_tokens":0,"cache_read_tokens":30214,"total_tokens":282}}}
{"event":"result","result":{"conversation_id":"9ec58bfd-4d67-4f5e-83a5-9d907e9c6b1f","status":"SUCCESS","response":"apple\n","duration_seconds":2.548755756,"num_turns":2,"usage":{"input_tokens":30662,"output_tokens":8,"thinking_tokens":0,"cache_read_tokens":30214,"total_tokens":30670}}}
The second turn answers apple from the first turn’s context. Notice that a single conversation_id tracks the entire session, and init is only sent once.
When parsing the result object, keep in mind that the response text applies only to the current turn, while metadata counters track the cumulative session:
| Field | Scope |
|---|---|
response | The turn that emitted it |
num_turns | Cumulative over the session |
usage | Cumulative over the session |
duration_seconds | Cumulative over the session |
To filter and only view the final responses, you can pipe the output through jq:
printf '%s\n' \
'{"event":"user","message":{"content":"Reply with exactly: one"}}' \
'{"event":"user","message":{"content":"Reply with exactly: two"}}' \
| agy --input-format stream-json --output-format stream-json \
| jq -r 'select(.event=="result") | "\(.result.num_turns): \(.result.response)"'
1: one
2: two
Drive a session programmatically
Section titled “Drive a session programmatically”Instead of passing all prompts up front, you can hold the stdin pipe open in a script. This allows your application to evaluate the model’s answer before submitting the next prompt.
Tip: You can read
stdoutline by line and dispatch logic based on theeventfield. Wait until you receive theresultevent for the current prompt before writing the next one.
For example:
import json
import subprocess
proc = subprocess.Popen(
["agy", "--input-format", "stream-json", "--output-format", "stream-json"],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
text=True,
bufsize=1,
)
def ask(prompt):
"""Send one prompt and return the response for that turn."""
message = {"event": "user", "message": {"content": prompt}}
proc.stdin.write(json.dumps(message) + "\n")
proc.stdin.flush()
for line in proc.stdout:
event = json.loads(line)
if event["event"] == "result":
return event["result"]["response"]
first = ask("Name one popular version control system. Answer with one word.")
print(ask(f"Name a competitor to {first.strip()}. Answer with one word."))
proc.stdin.close()
proc.wait()
End a session
Section titled “End a session”To close a session gracefully, simply close stdin. The process exits after the input pipe is closed and the current turn completes. If an application writes a final prompt and immediately closes the pipe, it still receives the final result before the process terminates.
Clean sessions exit with 0, which matches the standard headless mode behavior.
Unsupported messages
Section titled “Unsupported messages”To prevent unpredictable behavior, the CLI validates inputs. If it encounters a malformed or unsupported message, it responds as described in the following table:
| Input | Result | Exit |
|---|---|---|
Unrecognized event name | Skipped: warning logged to stderr | — |
control_request or control_response events | ERROR result, session ends | 2 |
Slash command handled by the CLI such as /model | ERROR result, session ends | 2 |
Message missing the event field | ERROR result, session ends | 1 |
| Invalid JSON line | ERROR result, session ends | 1 |
Content block type other than text | ERROR result, session ends | 1 |
Unrecognized event names are safely skipped with a warning. This ensures that applications built against newer versions of the streaming protocol won’t crash when running on older CLI versions:
warning: ignoring unsupported stream input message event "future_thing"
For all other errors, the session terminates immediately. Any turns that previously completed retain their result events, but the malformed input line aborts the remainder of the session.
Slash commands that the CLI can respond to directly (such as /model and /usage) produce a text report rather than a standard event stream. A streaming session cannot leverage these types of slash commands. For example:
{ "event": "result", "result": { "conversation_id": "4fae3a70-409d-42a4-86ea-9de206a49ff4", "status": "ERROR", "response": "", "error": "/model is answered by the CLI itself and is unavailable with --input-format stream-json; run it as its own --print /model invocation", "duration_seconds": 0, "num_turns": 0, "usage": { "input_tokens": 0, "output_tokens": 0, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 0 } } }
Common mistakes
Section titled “Common mistakes”| Mistake | Why it fails | How to fix it |
|---|---|---|
Pairing with --output-format json or text | Those formats only emit a single output envelope when the process exits, causing every turn except the last one to be lost. | Always use --output-format stream-json with this input mode. |
Passing a prompt with the -p flag | Streaming mode exclusively listens for prompts on stdin. Any prompt passed through a command-line flag is dropped. | Send the prompt into stdin as a user message instead. |
Sending /model or /usage into the stream | The CLI handles these commands internally outside of the event stream, breaking the JSON flow. | Run agy -p /model as an entirely separate, standalone command. |
Treating num_turns as a per-turn count | Metadata counters (like turns, duration, and usage) track the entire cumulative session, not just the active turn. | Use the response field to get the text for the current turn. |
Waiting for the process to exit before reading stdout | The session stays open indefinitely until stdin is closed. If your script waits for an exit signal, it will hang. | Read the events line-by-line as they arrive, and manually close stdin when finished. |
Select a model, effort, or agent
Section titled “Select a model, effort, or agent”List the available model slugs, then pin one for the run:
agy models
gemini-3.7-flash-high Gemini 3.7 Flash (High)
gemini-3.7-flash-medium Gemini 3.7 Flash (Medium)
gemini-3.6-flash-high Gemini 3.6 Flash (High)
gemini-3.6-flash-medium Gemini 3.6 Flash (Medium)
gemini-3.5-flash-medium Gemini 3.5 Flash (Medium)
gemini-3.1-pro-high Gemini 3.1 Pro (High)
claude-sonnet-4-6 Claude Sonnet 4.6 (Thinking)
...# Pin a model by slug.
agy -p "Reverse the string antigravity." --model gemini-3.5-flash-medium
# Set reasoning effort (low, medium, or high).
agy -p "Outline a plan to add caching to this service." --effort high
# Select an agent (list them with `agy agents`).
agy -p "Review this function for edge cases." --agent <agent-name>
Unlike the interactive UI, headless mode does not silently fall back when --model names an unknown model. It exits non-zero with an ERROR status so a pinned pipeline fails loudly instead of running the wrong model.
Permissions in headless mode
Section titled “Permissions in headless mode”There is no interactive prompt in headless mode, so tools that would normally ask for confirmation are handled by policy.
By default, the CLI respects the permission mode in your settings. A tool that requires approval it cannot obtain is soft-denied: the run continues, exits 0, and prints a notice to stderr naming the tool and how to allow it. Reading and writing files inside your active workspace is auto-allowed; actions such as shell commands default to Ask and are soft-denied in headless mode unless you grant them.
Grant a tool ahead of time by adding an action(target) rule under permissions.allow in ~/.gemini/antigravity-cli/settings.json:
{
"permissions": {
"allow": ["command(git)", "command(npm run (build|lint|test))", "write_file(src/)"]
}
}
To auto-approve every tool for a run, pass --dangerously-skip-permissions:
agy -p "Run the test suite and report failures" --dangerously-skip-permissions
Warning:
--dangerously-skip-permissionsapproves all tool calls, including file writes and command execution. Prefer scopedpermissions.allowrules unless you fully trust the prompt and environment. See Permissions for the full rule syntax.
Handle exit codes and errors
Section titled “Handle exit codes and errors”A successful run exits 0. A run that fails to produce a response exits non-zero and writes the reason to stderr. In json and stream-json modes, the failure also appears in the status and error fields.
For example, pinning an unknown model exits 1 and returns an error envelope:
agy -p "hi" --model does-not-exist-model --output-format json; echo "exit=$?"
{"conversation_id":"","status":"ERROR","response":"","error":"invalid model selection (--model \"does-not-exist-model\" --effort \"\"): model does-not-exist-model is not recognized as a known model or custom model in settings\nAvailable models:\n Gemini 3.6 Flash (High)\n ...","duration_seconds":0,"num_turns":0,"usage":{"input_tokens":0,"output_tokens":0,"thinking_tokens":0,"cache_read_tokens":0,"total_tokens":0}}
exit=1
The status field reports the terminal state of the run:
| Status | Meaning |
|---|---|
SUCCESS | The run completed and produced a response |
ERROR | The run ended with an error |
CANCELED | The run was canceled |
INTERRUPTED | The run was interrupted (for example, SIGINT) |
INVALID | The run reached an invalid state |
WAITING | The run ended while waiting on input |
RUNNING | The run did not reach a terminal state |
By default, a run waits up to five minutes for a response. Adjust the ceiling with --print-timeout:
agy -p "Summarize the design tradeoffs of optimistic locking." --print-timeout 15m
Flag reference
Section titled “Flag reference”| Flag | Default | Description |
|---|---|---|
-p, --print, --prompt | — | Run a single prompt non-interactively and print the response |
--output-format | text | Output format: text, json, or stream-json |
--input-format | text | Input format: text or stream-json; reads prompts on stdin |
--json-schema | — | Schema string or file path to enforce structured output |
--model | — | Model slug for this run (see agy models) |
--effort | — | Reasoning effort: low, medium, or high |
--agent | — | Agent for this run (see agy agents) |
--continue, -c | false | Continue the most recent conversation |
--conversation | — | Resume a conversation by ID |
--dangerously-skip-permissions | false | Auto-approve all tool permission requests |
--print-timeout | 5m | Maximum time to wait for a response |
--sandbox | false | Run with terminal sandbox restrictions enabled |
Example: run the agent in CI
Section titled “Example: run the agent in CI”Fail the job on error and save the response:
#!/usr/bin/env bash
set -euo pipefail
result=$(agy -p "Name three popular version control systems, comma-separated." \
--output-format json \
--print-timeout 10m)
status=$(echo "$result" | jq -r '.status')
if [[ "$status" != "SUCCESS" ]]; then
echo "Agent run failed: $(echo "$result" | jq -r '.error')" >&2
exit 1
fi
echo "$result" | jq -r '.response' > result.txt
Next steps
Section titled “Next steps”- Prompting & Interaction: Write effective prompts for the agent.
- Permissions: Configure allow, deny, and ask rules.
- Background Tasks & Subagents: Delegate work to specialized agents.
- Reference: Full command and flag reference.