Skip to content

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.

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 agy session first. In a non-interactive environment with no terminal (for example, CI), a run that is not already authenticated exits with an authentication required error instead of hanging.

The --output-format flag controls the shape of stdout. It accepts three values:

Formatstdout shapeUse it for
textThe response text (default)Human-readable output, quick scripts
jsonOne JSON object printed on completionCapturing a result plus metadata
stream-jsonNewline-delimited JSON (NDJSON) eventsMonitoring 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:

FieldTypeDescription
conversation_idstringID of the conversation, for resuming later
statusstringTerminal status (see Status values)
responsestringThe agent’s free-text response
errorstringError message; present only on failure
duration_secondsnumberWall-clock duration of the run
num_turnsnumberNumber of user turns in the conversation
structured_outputobjectParsed schema output; present only with --json-schema
json_schemaobjectThe schema that was enforced; present only with --json-schema
usageobjectToken counts: input_tokens, output_tokens, thinking_tokens, cache_read_tokens, total_tokens

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'

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:

eventPayload keyEmitted
initinitOnce, at stream start
step_updatestep_updateFor each step transition or text delta
resultresultOnce, 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):

FieldTypeDescription
cwdstringWorking directory
toolsstring[]Names of all available tools
permission_modestringEffective permission mode
modelstringModel in use, when overridden
agentstringActive agent, when overridden
json_schemaobjectEnforced 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:

FieldTypeDescription
conversation_idstringID of the conversation
step_indexnumberZero-based index of the step
statestringACTIVE or DONE
step_typestringStep category, for example agent_response or tool
tool_namestringCanonical tool name, on tool steps
text_deltastringIncremental response text
duration_secondsnumberStep duration, when known
usageobjectPer-step token usage, when known
tool_infoobjectTool invocation details (see below)
subagent_infoobjectSubagent invocation details

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).

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.

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 concatenating text_delta fragments so jq does not insert newlines between them.

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.

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-json requires --output-format stream-json. In a streaming session, the CLI emits exactly one result event per turn.

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.

The output stream works as follows:

  1. Opens with a single init event.

  2. Emits a series of step_update events for the active turn.

  3. Concludes the turn with a final result event.

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:

FieldScope
responseThe turn that emitted it
num_turnsCumulative over the session
usageCumulative over the session
duration_secondsCumulative 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

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 stdout line by line and dispatch logic based on the event field. Wait until you receive the result event 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()

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.

To prevent unpredictable behavior, the CLI validates inputs. If it encounters a malformed or unsupported message, it responds as described in the following table:

InputResultExit
Unrecognized event nameSkipped: warning logged to stderr
control_request or control_response eventsERROR result, session ends2
Slash command handled by the CLI such as /modelERROR result, session ends2
Message missing the event fieldERROR result, session ends1
Invalid JSON lineERROR result, session ends1
Content block type other than textERROR result, session ends1

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 } } }
MistakeWhy it failsHow to fix it
Pairing with --output-format json or textThose 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 flagStreaming 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 streamThe 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 countMetadata 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 stdoutThe 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.

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.

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-permissions approves all tool calls, including file writes and command execution. Prefer scoped permissions.allow rules unless you fully trust the prompt and environment. See Permissions for the full rule syntax.

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:

StatusMeaning
SUCCESSThe run completed and produced a response
ERRORThe run ended with an error
CANCELEDThe run was canceled
INTERRUPTEDThe run was interrupted (for example, SIGINT)
INVALIDThe run reached an invalid state
WAITINGThe run ended while waiting on input
RUNNINGThe 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
FlagDefaultDescription
-p, --print, --promptRun a single prompt non-interactively and print the response
--output-formattextOutput format: text, json, or stream-json
--input-formattextInput format: text or stream-json; reads prompts on stdin
--json-schemaSchema string or file path to enforce structured output
--modelModel slug for this run (see agy models)
--effortReasoning effort: low, medium, or high
--agentAgent for this run (see agy agents)
--continue, -cfalseContinue the most recent conversation
--conversationResume a conversation by ID
--dangerously-skip-permissionsfalseAuto-approve all tool permission requests
--print-timeout5mMaximum time to wait for a response
--sandboxfalseRun with terminal sandbox restrictions enabled

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