Usage¶
CLI command reference. For environment variables see CONFIGURATION. For auth see AUTHENTICATION.
⚠️ Status.
gflow videocommands are fully wired as of v0.2.0a1.gflow imagecommands (upload,t2i,i2i) are wired as of v0.3.0a1. v0.4.0a2 added per-class exit codes (3–7) for shell branching, JSON-on-pipe structured logs (GFLOW_CLI_LOG_FORMAT=json), per-worker batch concurrency (GFLOW_CLI_CONCURRENCY=N), and tenacity-driven retry/backoff on transient failures.
Synopsis¶
gflow [OPTIONS] COMMAND [ARGS]...
Commands:
docs Browse gflow's own documentation (offline, read-only).
(no args) List every topic and what it covers.
TOPIC Print one page as raw Markdown (pipes cleanly).
--search TERM Matching lines, with docs/FILE.md:LINE positions.
auth Manage Google sessions for Flow.
(no args) Show profile list, or trigger first login.
login One-time interactive sign-in.
status Prove the profile's Flow session live; exit 0 verified / 1 not.
list List every profile and indicate the default.
use NAME Set NAME as the default profile.
logout Delete a profile's saved session (asks first).
image Image generation (Imagen / Nano Banana via Flow).
upload Upload a local image and print its asset UUID.
t2i Generate 1-4 images from a text prompt.
i2i Generate 1-4 images from a prompt + reference(s).
video Video generation (Veo via Flow).
t2v Generate a video from a text prompt.
i2v Generate a video from an initial frame + motion prompt.
r2v Generate a video from reference images + prompt.
chain Render a JSONL manifest as a last-frame I2V chain.
scene Compose Flow Scenes (Add Clip) — credit-free REST.
create Compose ordered, trimmable clips into a scene (optional extended .mp4).
show Read back a scene's clip order and trims.
character Manage Flow Character entities for a project (#145).
create Generate a reusable character (paid face + body refs).
list List the characters in a project.
show Show one character by --id or --name.
rm Delete a character by --id or --name (FREE).
voices List the preset Gemini voices for character TTS.
data Local provenance database (read-only queries).
media MEDIA_ID Show stored record for a Flow media ID.
list {projects,images,videos,profiles} Browse the catalog.
prune Remove stale local file entries.
credits Inspect current Flow balances (user / list).
models Print the image/video model catalog (Rich table or --json).
project Manage Flow projects (create / rename / list).
instructions Persistent Agent-Mode brief cards (add/list/enable/disable/rm/apply/toggle-mode).
movie Multi-scene manifest pipeline (run / template).
tools Prompt-rewriting tools (list / show / run; also --tool on generation commands).
run Config-driven generation run.
mcp MCP server over stdio (run) + client-config generator (setup).
serve MCP server over Streamable HTTP at /mcp.
update Upgrade gflow-cli in place via uv tool / pipx / pip (--check to only report).
Global flags:
-V,--version— print version and exit.-v,--verbose— log at DEBUG level.
Machine-readable output: the generation commands (image t2i / image i2i,
video t2v / i2v / r2v), auth list, credits user / credits list, and
gflow models accept --json to
emit a single parseable object on stdout instead of Rich tables. See
§ JSON output.
Note: --profile NAME is per-subcommand, not global — pass it after the subcommand name (e.g. gflow image t2i "..." --profile experiments, not gflow --profile experiments image t2i ...).
gflow docs¶
The documentation, from the terminal. Read-only, offline, no account, no credits — the pages ship inside the package, so this works on a machine that has never seen this repository.
gflow docs # every topic, with what it covers
gflow docs usage # print one page (raw Markdown — pipe it)
gflow docs --search "r2v duration" # find the line that answers a question
| Flag | Effect |
|---|---|
--search TERM |
Every line mentioning TERM, with docs/FILE.md:LINE next to it. Whitespace splits the query into terms that must all appear on the line |
--json |
Machine-readable, on all three forms |
A topic name is whatever you would type: the slug (user-guide), the file name
(USER_GUIDE.md), either case, or any unambiguous prefix (conf → configuration). An
ambiguous prefix lists the candidates; an unknown name is matched against the topic
list with difflib, so a typo (usge) is still offered usage.
Use two words when one is common. --search duration matches well over a hundred
lines; the table header carries the total, shows the first 20 and says how many it held
back. --search "r2v duration" narrows that to a handful with the host-specific rule
at the top.
Curated answers rank first: the **"How do I …?"** → rows in
INDEX were each written to be an answer, so they come above
raw body-text matches for the same term.
Piping works — the page is emitted as its own Markdown, not as rendered boxes:
gflow docs configuration | grep GFLOW_CLI_STORAGE_URI
gflow docs --search "exit code" --json | jq -r '.matches[].path'
There is deliberately no MCP twin — see the note at the top of
src/gflow_cli/cli_docs.py.
gflow auth¶
See AUTHENTICATION § Commands.
gflow credits¶
Read the current Google Flow balance through an existing authenticated profile. This is a read-only request: it does not generate media or spend credits. The displayed balance funds Veo video generation; image generation uses separate per-model daily quotas.
The balance is read from flow.google.com: gflow opens the app in the profile's browser and reads
the number the app itself loads, which is the one Flow's account menu shows
(#795). Nothing is clicked. It costs one
browser launch per profile, and the profile must not be in use by another gflow process
(ProfileLockedError, exit 11). Before #795 the balance came from a labs.google token; labs'
session URL has answered HTTP 410 to every account since 2026-10-22
(#1023).
--refresh renews a signed-out session instead of reporting it. When the read lands on a
Google sign-in page instead of the app, gflow opens that profile in a headless browser, loads Flow so Google and Flow
rotate the saved cookies (the same renewal as gflow auth renew),
then reads the balance again. That costs one more Chrome launch per signed-out profile and no
credits. A session Google has revoked cannot be renewed, so the row
keeps its original diagnosis. A profile another gflow process is using is recorded as
ProfileLockedError and the list carries on. --refresh does not clear Flow's public /about
page (see KNOWN_ISSUES); that row keeps its original diagnosis.
credits user applies the normal profile precedence chain. credits list inspects every saved
profile sequentially and returns successful balances even when another profile is expired or
unavailable. Its JSON envelope contains profiles, total_credits, and count; each profile
includes authenticated, credits, subscription_credits, user_paygate_tier, service_tier,
and sku. flow.google.com does not report a tier, so subscription_credits, user_paygate_tier,
service_tier and sku are always null; the keys stay so existing parsers keep working.
Failed profiles have authenticated: false, a safe error name, and a null balance.
The balance field was matched against Flow's account menu on two accounts, neither on a paid plan. A paid plan may carry a different balance in that field, which would show a wrong number without an error.
The failures are typed. A sign-in landing is exit 3 (AuthExpiredError, fixable with
--refresh). Flow's public /about page and an app that loads but never fetches the balance
are exit 31 (FlowAppError). An account without Flow access is exit 39. A balance reply in a
shape gflow does not recognise is exit 7 (WireFormatError). None of them tells you
to re-authenticate a session that is working. No cookie, token or browser API key is printed
or stored.
gflow image upload¶
Upload a local PNG/JPEG/WebP/GIF into a fresh Flow project and print the asset UUID + dimensions Flow inferred. The UUID is what later subcommands (gflow image i2i --ref UUID, video i2v) accept as an initial frame. Note for video i2v: the UUID is preferentially selected from the generation project's media picker, and upload puts the asset in a fresh scratch project — pass --project <id> of the project that holds the asset to select it in place. Since v0.58.0 (#529) a picker miss falls back to re-uploading the catalog's recorded local file (integrity-verified); exit 9 now only occurs when neither the tile nor a verified local file is available.
gflow image upload PATH [OPTIONS]
Arguments:
PATH Local image file (PNG, JPEG, WebP, or GIF). [required]
Options:
--profile NAME Profile name (overrides default).
The uploader validates the file's magic bytes (PNG \x89PNG, JPEG \xff\xd8\xff, WebP RIFF...WEBP, or GIF87a/89a) before calling Flow — anything else is rejected client-side. There is also a hard 20 MB size cap to match Flow's documented per-file limit; oversize files fail fast without burning a network round-trip.
Examples:
# Upload and read the printed UUID
gflow image upload hero.png
# Pick a profile explicitly
gflow image upload ./shots/01.jpg --profile experiments
Output (truncated):
Capture the UUID into a shell variable to chain into i2i or video i2v:
UUID=$(gflow image upload hero.png | awk '/Asset UUID:/ {print $3}')
gflow image i2i "make it cinematic" --ref "$UUID"
gflow image upscale¶
Upscale a platform-generated image to 2K or 4K (the same 1K/2K/4K options Flow's download menu offers) and save it locally. Uploaded images are not supported.
When Flow serves
flow.google.com(#639): gflow drives the image detail view's download menu, which answers on theSPrCadbatchexecute wire with the upscaled JPEG. When Chrome drops that reply, gflow takes the same JPEG from the page's own download (#1027). Verified for 2K on 2026-10-07; 4K was seen only as a disabled option (exit 22) on the account used.
gflow image upscale MEDIA_ID --scale 2k|4k [OPTIONS]
Arguments:
MEDIA_ID UUID of a Flow-generated image (find one with
`gflow data list images`).
Options:
--scale 2k|4k Target resolution (required). 4k requires a Flow Ultra
subscription; 1k is the original (no upscale).
--project ID Project that owns the image. Resolved from the local
catalog when omitted; pass it explicitly for images
gflow didn't record (e.g. generated in the web UI).
--out PATH Output directory (see "Output paths" below).
--profile NAME Profile name (overrides default).
--transport ui_automation Override transport strategy (advanced).
Examples:
gflow image upscale 3a56bb5e-92a2-44f4-9992-3c6a9bf0cd14 --scale 2k
gflow image upscale <mediaId> --scale 2k --project <projectId>
Notes:
- Credit-free — upscaling is an image operation and spends no credits.
- 4K is Ultra-only. On a non-Ultra account a 4K request fails with exit code 22
(
UpscaleUnavailableError, detected from the disabled UI state or 403) and a hint to use--scale 2kor upgrade. - The result is saved as
<output_dir>/images/<YYYY-MM-DD>/<mediaId>_<scale>.<ext>(extension matches the returned format — usually.jpg).
gflow video upscale¶
Upscale or export a platform-generated video to 1080p Full HD, download the original 720p, or export as a 270p animated GIF and save it locally.
gflow video upscale MEDIA_ID [OPTIONS]
Arguments:
MEDIA_ID UUID of a Flow-generated video (find one with
`gflow data list videos`).
Options:
--scale [1080p|720p|270p] Target quality: 1080p (enhanced Full HD), 720p (original download),
or 270p (animated GIF export). [default: 1080p]
--project ID Project that owns the video. Resolved from the local
catalog when omitted; pass it explicitly for videos
gflow didn't record (e.g. generated in the web UI).
--out PATH Output directory (see "Output paths" below).
--profile NAME Profile name (overrides default).
Examples:
gflow video upscale 00000000-0000-4000-8000-000000000002 --scale 1080p
gflow video upscale 00000000-0000-4000-8000-000000000002 --scale 270p
Notes:
- Quality options: 720p is the original generation download, 1080p is the enhanced Full HD export,
and 270p exports an animated GIF. A saved MP4 is checked to really be at least the
requested resolution, so a preview or the 720p original is refused (exit 7,
WireFormatError) rather than saved as the export. - flow.google.com only. There is no labs.google route;
GFLOW_CLI_FLOW_HOST=labs.googlerefuses the command with exit 36. - Cost: the 1080p export was measured free (1 observation, 2026-10-07).
- Saved as
<output_dir>/videos/<YYYY-MM-DD>/<mediaId>_<scale>.<ext>(.mp4or.gif).
gflow image t2i¶
Generate 1–4 images from one text prompt, or run a shell-friendly batch of 1–50 prompts through one Flow session/project.
Migrated
flow.google.comaccounts (#639): T2I is supported with Nano Banana 2.1 (nano2, #958), Nano Banana 2 Lite (nano2-lite) and Nano Banana Pro (nano-pro), all five aspects (16:9,4:3,1:1,3:4,9:16), and count 1–4. Without--project(and withoutGFLOW_CLI_PROJECT), gflow creates a fresh project there first (#864). The migrated page owns its reCAPTCHA +ogiZ0bsubmit. Imagen 4, Agent instructions, character/entity references andimage batchremain unavailable on that host and fail before submit. On Flow's agent-only composer (#799), t2i runs through the agent driver; seeGFLOW_CLI_AGENT_CONFIRM.
gflow image t2i PROMPT [PROMPT ...] [OPTIONS]
gflow image t2i --prompts-file FILE [OPTIONS]
gflow image t2i --stdin [OPTIONS]
Arguments:
PROMPT Text prompt. Repeat for multi-prompt mode.
Options:
--prompts-file FILE UTF-8 text file: one prompt per non-empty line;
whole-line # comments skipped.
--stdin Read prompts from stdin using the same format.
--continue-on-error /
--fail-fast Continue after per-prompt failures or stop at the
first failed prompt. [default: continue-on-error]
--jitter SPEC Anti-bot pause between multi-prompt submissions:
'MIN-MAX' seconds, a single number for 0-N, or 0
to disable. Widen (e.g. 10-30) if runs hit WAF
403s. [default: 0.5-1.5; GFLOW_CLI_JITTER_RANGE
overrides the default]
--model [nano2|nano2.1|nano-2.1|nano-pro|nano2-lite|image4|imagen4]
Image model alias. [default: nano2]
--aspect [9:16|16:9|1:1|4:3|3:4]
Aspect ratio. [default: 9:16]
-n, --count INTEGER How many images to generate (1-4). [default: 1]
--out PATH Output directory (see "Output paths" below).
--project ID Generate in this EXISTING Flow project instead of a
scratch project. Required to reference locked
entities/assets in that project. With several
prompts, every prompt runs in it.
--project-name, --project-title TEXT
Title for the fresh project gflow creates when
--project is omitted (single- or multi-prompt).
-o, --output PATH Explicit destination file path for the generated asset
(e.g., `./out/bird.png`). Overrides automatic filename.
--reference-entity ID Flow CHARACTER entity id to reference for character
consistency (repeatable; must live in --project).
Single-prompt only.
--reference-entity-name N Display name paired with --reference-entity.
-t, --tool NAME[:k=v] Apply a prompt tool before generating, e.g.
`creative-director:style=cinema`. Repeatable; applied
per prompt on multi-prompt/batch. See "Prompt tools".
--ui-mode [auto|classic|agentic]
Require a Flow UI arm: classic (hard aspect
controls) / agentic (chat surface; forced by -i) /
auto (default) — auto resolves to classic, the only
arm that can satisfy an image request (#595).
Aborts exit 28 if unreachable.
Single-prompt only; batch uses GFLOW_CLI_UI_MODE.
--profile NAME Profile name (overrides default).
Models:
| Alias | Backing model | Notes |
|---|---|---|
nano2 (also nano2.1, nano-2.1) |
Nano Banana 2 (NARWHAL) |
Default. Fast, balanced quality. On flow.google.com Flow now offers Nano Banana 2.1 (wire BELUGA) in its place, and nano2 selects it (#958). The 2.1 spellings select the same tier (#1038); a labs request for that tier carries NARWHAL (last seen there as Nano Banana 2). |
nano-pro |
Nano Banana Pro (GEM_PIX_2) |
Higher quality, slower. |
nano2-lite |
Nano Banana 2 Lite (HARBOR_SEAL) |
Lightweight Nano Banana 2 variant. Its i2i reference cap is a provisional 3 and its daily quota is unmeasured (#787). |
image4 |
Imagen 4 (IMAGEN_3_5) |
Photoreal-leaning Imagen variant. No longer in Flow's model menu (absent since 2026-08-26), and the flow.google.com composer refuses it before submit. |
Multi-prompt shortcut.
- Positional multi-prompt:
gflow image t2i "p1" "p2" "p3". --prompts-file FILE: UTF-8 text, one prompt per non-empty line, whole-line#comments skipped.--stdin: same format as--prompts-file.- Sources are mutually exclusive.
- Output names use
prompt_<prompt-index>_<variation-index>.png. - With
-n 4, each prompt produces four images; the maximum shell shortcut fan-out is 50 prompts * 4 = 200 images. --continue-on-erroris default;--fail-faststops after the first failed prompt.
Prompt tools (-t / --tool).
A tool is a named, single-purpose transform applied to your prompt before generating —
the first built-in is creative-director, which rewrites a terse prompt into a richer one
using Google's five-component formula (Subject + Action + Context/Location +
Composition/Camera + Style) via the public Gemini API. -t/--tool is repeatable and
works on every generation command (image t2i/i2i/batch, video t2v/i2v/r2v/chain);
on multi-prompt/batch it is applied per prompt.
- Requires an OpenAI-compatible endpoint:
GFLOW_CLI_LLM_API_KEY(Google key) and/orGFLOW_CLI_LLM_BASE_URL. The model is set per tool via the TOMLconfig.modelkey (defaultgemini-2.5-flash). - Graceful: if the key is unset or the API errors, gflow prints a notice and generates from your original prompt — the run never fails because of a tool (each call is bounded by an overall ~60s wall-clock budget).
- The local catalog records both the original prompt and the submitted
expansion (withheld under
GFLOW_CLI_HISTORY_PROMPTS=redacted), plus ametadata_json.toolprovenance descriptor. - Discover and preview tools with
gflow tools list/show/run(see thegflow toolssection below). Full reference: TOOLS.md and PROMPT_EXPANSION.md.
# Rewrite "cat in space" into a detailed prompt with the cinema style, then generate
gflow image t2i "cat in space" --tool creative-director:style=cinema
Output paths.
- Explicit File (
-o/--output PATH). Saves the generated asset directly toPATH(e.g.-o ./assets/hero.png). Parent directories are created automatically if they do not exist. With--count > 1, images are written asPATH's stem plus_1,_2, … before the extension (-o hero.png --count 2→hero_1.png,hero_2.png); video commands write a single file only.-ois single-prompt only (multi-prompt runs abort with a usage error — use--outthere) and takes precedence over--outwhen both are passed. The path is a local filesystem path; it does not accepts3:///gs://URLs (cloud output still goes throughGFLOW_CLI_STORAGE_URI). - Default (
--outomitted). Files land under$GFLOW_CLI_OUTPUT_DIR/images/<YYYY-MM-DD>/<media_name>_<n>.png. The date partition keeps long-running batches navigable. - Directory (
--out DIR). Files are written flat as<DIR>/<media_name>_<n>.png— no date subdirectory. - Multi-prompt mode. Files are written as
prompt_<prompt-index>_<variation-index>.png; the prompt index and variation index are zero-based.
Examples:
# Single image, default model + 9:16 aspect
gflow image t2i "a serene mountain lake at dawn"
# 16:9 with the higher-quality model
gflow image t2i "neon cyberpunk alley" --model nano-pro --aspect 16:9
# 4 variations of a logo at 1:1, written flat into ./logos/
gflow image t2i "variations of a minimalist fox logo" -n 4 --aspect 1:1 --out ./logos
# Three prompts in one warm Flow session/project
gflow image t2i "p1" "p2" "p3" --aspect 16:9 --model image4
# Text file input: comments and blank lines are ignored
gflow image t2i --prompts-file prompts.txt --fail-fast
# Pipeline input
Get-Content prompts.txt | gflow image t2i --stdin
A 4-image run with --out ./logos/ produces:
./logos/<media_name>_1.png
./logos/<media_name>_2.png
./logos/<media_name>_3.png
./logos/<media_name>_4.png
(<media_name> is the per-image UUID Flow assigns; the _<n> suffix is the 1-based index in the batch.)
gflow image i2i¶
Generate 1–4 images by blending a text prompt with one or more reference images. Same flag set as t2i, plus a required --ref (repeatable).
Migrated
flow.google.comaccounts (#639): local-file--refvalues are supported and each uploaded media id is verified in the outgoingogiZ0bbody. Without--project, gflow creates one there first (#864). UUID refs,@Name/--reference-entity, Agent instructions and Imagen 4 remain unavailable on that host and fail before submit rather than silently degrading to T2I.
gflow image i2i PROMPT --ref PATH_OR_UUID [--ref ...] [OPTIONS]
Arguments:
PROMPT Text prompt. [required]
Options:
--ref PATH_OR_UUID Reference image. Repeat for multiple. [required]
--model [nano2|nano2.1|nano-2.1|nano-pro|nano2-lite|image4|imagen4]
Image model alias. [default: nano2]
--aspect [9:16|16:9|1:1|4:3|3:4]
Aspect ratio. [default: 9:16]
-n, --count INTEGER How many images to generate (1-4). [default: 1]
--out PATH Output directory (same semantics as t2i).
-o, --output PATH Explicit destination file path (same semantics as t2i).
--project ID Generate in this EXISTING Flow project (skip scratch).
--reference-entity ID Flow CHARACTER entity id to reference (repeatable;
must live in --project). Counts toward the ref cap.
--reference-entity-name N Display name paired with --reference-entity.
--profile NAME Profile name (overrides default).
Path-or-UUID semantics. Each --ref value is classified at the CLI boundary:
- Looks like a Flow asset UUID (case-insensitive 8-4-4-4-12 hex, e.g.
ddb6ef97-262d-49f4-8269-4a28c0fae6a2) → resolved through the local catalog (v0.58.0, #529): the asset's recorded Flow display name is searched in the project's reference picker and the exact UUID tile is selected — no duplicate upload, no grid scrolling, no UUID/prompt-text searches. A stale cached name self-heals (#546): on a picker miss in a known project, one free ~0.5 s listing fetch resolves the current name by UUID, the search is retried once, and the catalog is updated (sync.source = "refresh") — a rename in the Flow UI costs one extra request, once. When the tile still can't be reached (different project, no recorded name), the catalog's recorded local file is uploaded instead — only after its byte count and SHA-256 still match the recording; otherwise the run aborts with a typed error rather than attaching the wrong bytes. - Anything else → treated as a local path. The CLI canonicalises it (resolving symlinks once at validation time, closing the symlink-laundering vector where
./hero.png -> ~/.ssh/id_rsacould exfiltrate secrets), then attaches it — deduplicated by filename since v0.38.0 (#314): if the target project's library already holds an asset with the exact same filename, that existing tile is selected in the picker instead of re-uploading, so repeating a ref across runs no longer piles up duplicate library entries. On a picker miss the file is uploaded as before. On flow.google.com, which has no such filename search, the first upload is recorded in the local catalog instead (#772): a later run in the same project with the same file (same path, same bytes) mentions that image in place, and if it was deleted from the project since, the file is uploaded again and the new copy recorded. Caveat for the filename match: it is by exact filename only — a different image that happens to share the name of one already in the project will be reused, not uploaded; rename the file if you need a fresh upload. (Videor2vrefs keep upload-only behavior.)
Mix and match in a single call. UUIDs and paths can co-exist on the same command line; order is preserved so imageInputs[] matches the order you typed.
Examples:
# Single ref by path (auto-uploaded)
gflow image i2i "make it cinematic, golden hour" --ref hero.png
# Two refs, both by path
gflow image i2i "blend these two compositions" --ref a.png --ref b.png
# Already-uploaded asset by UUID
gflow image i2i "stylize this asset" --ref ddb6ef97-262d-49f4-8269-4a28c0fae6a2
# Mix: one path, one UUID
gflow image i2i "mix references" --ref hero.png --ref ddb6ef97-262d-49f4-8269-4a28c0fae6a2
# 4-image fan-out from one ref, written flat
gflow image i2i "4 variants of this scene" --ref hero.png -n 4 --out ./variants
A 2-image run produces files numbered _1.png, _2.png:
Character-consistent images (entity references)¶
--reference-entity makes image t2i/i2i reference a locked Flow CHARACTER
entity (minted via gflow character create) so the generated subject stays
on-model across shots — no per-shot prompt wrangling or fragile image refs.
How it works: entities are project-scoped, so you must generate in the
project that owns them via --project <id> (this also means no throwaway
scratch project is created). The CLI attaches each entity through the Flow editor's
resource picker; the submit then carries referenceEntities, exactly like the
video R2V path. Entities count toward the same per-model reference cap as --ref
images. --reference-entity / --output are single-prompt only; --project also
works with several prompts, which then all run in that project.
For a pure character reference (no starting image) use t2i — i2i still
requires at least one --ref.
# One locked character, consistent across runs (t2i = no image ref needed)
gflow image t2i "Aria explores a neon market, full body" \
--project 7fa97443-… --reference-entity 8a77f8cb-… --reference-entity-name Aria \
--model nano2 --aspect 9:16 -n 3
# Several characters in one scene (each --reference-entity repeatable)
gflow image t2i "Aria and Drako meet at the gate" --project 7fa97443-… \
--reference-entity 8a77f8cb-… --reference-entity 4ed5cb7f-…
# i2i: a starting image PLUS an entity for the character
gflow image i2i "place this hero in a snowy pass" --ref hero.png \
--project 7fa97443-… --reference-entity 8a77f8cb-…
Ids are validated at the CLI boundary (letters/digits/hyphens, ≤128 chars). The entity must already exist in
--project(seegflow character create).
Referencing saved assets by name (@Name mentions)¶
Instead of ids or paths, you can reference a saved Character — or, on image
paths, a saved media asset — inline in the prompt by name, prefixed with @.
The mention is resolved against the project's assets, staged through the same
reference machinery as --reference-entity / --ref, then stripped from the text
the model sees. Shipped in v0.40.0 (#344).
# One saved Character by name (t2i needs no image ref)
gflow image t2i "@CaptainZoro on a rain-soaked neon rooftop, cinematic" --project <id>
# Two saved Characters in one video prompt (each staged as a reference, cap-checked)
gflow video r2v "@Zoro hands @Mika the sword" --project <id>
Where @Name works — character mentions on image and video; media/asset
mentions on image only:
| Path | Character @Name |
Media/asset @Name |
|---|---|---|
image t2i |
✅ | ✅ (in-project asset → referenceImages) |
image i2i |
✅ | ✅ |
video t2v / i2v / r2v |
✅ | ❌ — media-on-video is Phase 3; fails fast with exit 11 |
Rules:
- --project <id> is required — mentions resolve against that project's assets.
- A name resolves against Characters (gflow character create) and saved
media assets; on a name clash the Character wins. Matching is
case-insensitive.
- A Character must have at least one reference image before it can be tagged,
or the mention fails early.
- Write a literal @ by doubling it: @@ → @ (e.g. "ping me @@ 5pm" →
ping me @ 5pm). An @ glued to a preceding word (user@host) is never a
mention, so emails and handles are safe.
- Unknown or ambiguous names fail fast with exit 11 (ConfigurationError),
listing the available or colliding assets. If the asset catalog itself cannot be
loaded you get exit 29 (MentionIndexUnavailableError) — so scripts can tell
"catalog unreachable" apart from "no such name".
@Name and --reference-entity <id> resolve to the same wire
(referenceEntities) and dedupe against each other: reach for @Name for the
inline, by-name path — and --reference-entity when you'd rather pin
an explicit entity id in a script. Both are available on t2i/i2i and on
video t2v/r2v; video i2v takes neither, because its DTO rejects reference
entities (it carries start/end frames instead). --ref is different: it
attaches an arbitrary image (referenceImages), not a saved identity, and can
be combined with a @Name for "this identity, in this look".
See REFERENCE_STRATEGIES.md for the full decision guide.
gflow image batch¶
Generate multiple images from a single manifest file. The format is dispatched by file extension (.json or .tsv).
TSV manifest¶
Tab-separated columns. Only prompt is required; remaining columns fall back to the CLI defaults.
Lines starting with # and blank lines are skipped. Example: test_assets/sample_batch.tsv.
a small calico kitten sitting on a windowsill
a watercolor sunset over rolling hills 2 16:9
an isometric pixel-art bakery 1 1:1 nano2
JSON manifest¶
gflow image batch refuses any ref or reference_entity in a row (exit 11) —
"ref": "batch:N" and local files included. Use gflow run --config
(earlier row, local file).
[
{"text": "a small calico kitten sitting on a windowsill"},
{"text": "a watercolor sunset over rolling hills", "count": 2, "aspect_ratio": "16:9"},
{"text": "an isometric pixel-art bakery", "count": 1, "aspect_ratio": "1:1", "model": "nano2"}
]
Example: test_assets/sample_batch.json.
Session behaviour¶
All prompts in a batch share one Flow project. The editor is opened once and stays mounted; the transport is strictly serial — each prompt is configured, submitted, and its generation awaited before the next prompt is submitted, so only one generation is in flight at a time. The small random pause between submissions (default 0.5–1.5 seconds; tune with --jitter or GFLOW_CLI_JITTER_RANGE) is a submission-cadence anti-bot-detection measure on top of that serial rhythm. The command returns once every row has resolved (success or failure). See DEBUGGING § WAF cadence for when to widen the range.
Flags¶
--continue-on-error/--fail-fast— keep going past row failures or stop at the first one (default:--continue-on-error, matching the CLI flag default). On fail-fast, already-completed images are downloaded before the error is surfaced.--jitter SPEC— anti-bot pause between submissions:MIN-MAXseconds (e.g.10-30), a single number for0–N, or0to disable. Default0.5-1.5; widen if runs hit WAF 403s.GFLOW_CLI_JITTER_RANGEoverrides the default, the flag beats both.
Limits¶
MAX_BATCH_PROMPTS = 5(defined insrc/gflow_cli/image_batch.py). To raise, edit the constant.
Exit codes¶
0— all rows succeeded.1— invalid manifest (file not found, parse error, unknown aspect/model).- non-zero (other) — transport-level failure.
Observability¶
gflow image batch emits four structlog events per run, useful for debugging throttling regressions:
image_batch.submission_attempt {row_idx, prompt_hash, aspect, model, jitter_enabled, t_since_prev_submit_ms, project_id}image_batch.submission_result {row_idx, outcome, latency_ms, ...}image_batch.row_completed {row_idx, file_path, sha256_prefix}(per image)image_batch.inter_submission_latency_ms {row_idx, latency_ms}(fires from row 1 onward)
Shared video flags (
t2v/i2v/r2v):--model [omni-flash|veo-lite|veo-fast|veo-quality|veo-lite-lp](omit → Flow's current UI default),--duration [4|6|8|10](4/6/8 for Veo 3.1; 4/6/8/10 for omni-flash),--count INTEGER(1–4; >1 multiplies credit cost),--aspect [9:16|16:9],--profile NAME,--out-dir DIR(defaulttmp/).--durationsupport across models: Flow's settings popover is model- and cohort-conditional:omni-flashrenders a4s/6s/8s/10srow, while Veo 3.1 models accept4s/6s/8swhere the current Flow account/cohort exposes those controls (positive capture: a third, contributor-owned account onlabs.google, 2026-09-04, PR #650; the historical negative matrix onmy-profileremains valid for its cohort). 10s is reserved exclusively foromni-flash. Passing--duration 10with a Veo model fails fast with exit 2 before any browser work. On a cohort where Flow renders no duration control for Veo, omit--durationto accept Flow's default.--countis enforced fail-closed: if Flow's count control cannot be located (selector drift), the run refuses with exit 23 before submitting instead of proceeding on Flow's sticky default (typically x2) and silently changing what the run bills.t2vandi2v(notr2v) additionally take-o, --output PATH(explicit file destination). The mp4 lands at<output_file>or<out-dir>/<media_id>.mp4.
i2vmodel rules (issues #125 / #626): every model —omni-flashincluded — supports i2v with a start frame and with an end frame (--end-frame, first+last interpolation). The default isveo-lite(not Flow's UI default) because it is the cheapest, not because of any capability edge.--model omni-flashadditionally unlocks--duration 10for i2v. History: omni-flash was excluded from i2v entirely after a 2026-05-30 wire capture showed Flow silently dropping the frames and billing the run as text-to-video. A 2026-08-03 route-aborted re-capture (scripts/dev/capture_i2v_intercept_submit.py --model omni-flash --start-only) proved Flow now routes omni + start frame tobatchAsyncGenerateVideoStartImagewith the frame bound, and a live x1 10s generation confirmed the output interpolates from the start frame. The end frame followed once Flow shipped first+last for Omni 1.1 Flash: on 2026-09-02 the same probe without--start-onlycapturedbatchAsyncGenerateVideoStartAndEndImagewith bothstartImageandendImagenon-null, reproduced on two accounts at zero credits (#626). Instead of a static per-model table, gflow now checks the route Flow actually used after submit: a run whose end frame was silently dropped fails loudly rather than reporting a clip that ignored it as a success.
gflow video t2v¶
Generate a video from a text prompt only. On Flow's agent-only composer (#799), t2v runs
through the agent driver (one 9:16 or 16:9 clip); see GFLOW_CLI_AGENT_CONFIRM.
gflow video t2v PROMPT [--model] [--duration] [--resolution] [--count] [--aspect] [--ui-mode] [--profile] [-t/--tool] [--project] [--out-dir] [-o/--output]
Options:
--resolution [360p|720p]
Video resolution ('360p' or '720p', supported on omni-flash; on any other
model, or on the labs editor, the run stops before submit with exit 11
and no credits spent).
Omit for Flow's default.
-o, --output PATH Explicit destination file path for the generated asset
(e.g., `./out/clip.mp4`). Overrides automatic filename.
--project ID Generate in this EXISTING Flow project instead of a
scratch project (see "Sharing one project across calls").
--ui-mode [auto|classic|agentic]
Which Flow UI arm to require (#299). Video only has a
classic driver: classic/auto verify the classic editor
pre-submit and abort with exit 28 ($0 spent) if it is
unreachable; agentic is rejected (exit 2) — not yet
supported for video. Also on `video i2v`.
Two Flow frontends (#639). Under the default
GFLOW_CLI_FLOW_HOST=auto,t2vwith--project <id>runs on Flow's migratedflow.google.comhost on every account; without--project, gflow first creates a project — through labs.google where that route answers, otherwise on flow.google.com (#864) — and the run then routes exactly as if you had passed it.i2vwith a local--initial-frameruns there too — including with a local--end-frame(start+end interpolation, #639; seegflow video i2v) — as doesr2vfrom local--reffiles (seegflow video r2v). An end frame or start frame given by UUID or@Name, and video character references (@Nameor--reference-entity), are not ported to the migrated composer yet.image t2iand local-fileimage i2ialso run on a moved account, and on images a character can be tagged with@Nameor--reference-entity(see REFERENCE_STRATEGIES § flow.google.com; video is unchanged). An image--ref <uuid>is referenced in place, with no upload, when the local catalog records that image in the same--projectand keeps its Flow caption (#995); any other UUID, and instruction/Imagen-4 image forms, still exit 36.flow.google.comforces the migrated composer,labs.googleswitches it off — see CONFIGURATION § GFLOW_CLI_FLOW_HOST.
gflow video t2v "Slow cinematic push-in toward a candle flame"
gflow video t2v "Aerial shot of a coastline at sunset" --aspect 16:9 --out-dir ./out
gflow video t2v "A neon city timelapse" --model omni-flash --duration 10 --count 2
# Apply the creative-director tool (cinematic style) before generating
gflow video t2v "a dog surfing" --tool creative-director:style=cinematic
-t / --tool applies a prompt tool before generating — see
prompt tools under image t2i for the full contract
(requires GFLOW_CLI_LLM_API_KEY and/or GFLOW_CLI_LLM_BASE_URL; degrades gracefully to the original prompt).
gflow video i2v¶
Generate a video from an INITIAL frame (+ optional END frame) and a motion prompt.
Each frame is a local PNG/JPEG or the media UUID of an existing in-project asset
(#287 — the asset is selected in place, no duplicate upload; pair with --project
so the UUID's project is the one being generated in). A local file is bound into
the editor's frame slot via the media dialog, then Flow fires
batchAsyncGenerateVideoStartImage (initial only) or …StartAndEndImage
(initial+end interpolation).
On Flow's migrated
flow.google.comhost (#639) — a moved account, orGFLOW_CLI_FLOW_HOST=flow.google.com— only a local--initial-frameis served, with--project <id>: gflow uploads the file through the editor's own Upload entry (it stays in the project's library like any upload — a second run uploads it again), finds it in the Start-frame picker under the name it was uploaded with (see below), and refuses to submit unless the app's own submit body carries that upload's media id with an image-to-video model key (exit 7 otherwise: the labs #125 shape, where an unbound frame silently goes out as text-to-video). A UUID or@Nameframe exits 36 there; a local--end-frameis ported (#639). An account served labs.google keeps the labs driver for the unported forms.What lands in the library is a run-unique copy —
hero.pnguploads ashero-a1b2c3d4.png(#792). The picker is searched by display name, so identical names from earlier runs used to be indistinguishable and the search could bind a stale one; the random tag makes the match exact by construction. Expect the tagged names when you browse the project's library on flow.google.com.
gflow video i2v --initial-frame INITIAL [--end-frame LAST] PROMPT [--model] [--duration] [--resolution] [--count] [--aspect] [--ui-mode] [...]
# Back-compat positional form (still supported):
gflow video i2v IMAGE PROMPT [--end-frame LAST] [...]
Arguments:
PROMPT Motion prompt. [required]
Options:
--initial-frame PATH|UUID Initial frame to animate: local image path or in-project
asset media UUID. Canonical form; replaces the positional IMAGE.
--end-frame PATH|UUID Optional end frame — Flow interpolates initial frame -> end frame.
--resolution [360p|720p] Video resolution ('360p' or '720p', supported on omni-flash; on any other
model, or on the labs editor, the run stops before submit with exit 11
and no credits spent).
Omit for Flow's default.
--project ID Generate in this EXISTING Flow project instead of a
scratch project (see "Sharing one project across calls").
-o, --output PATH Explicit destination file path for the mp4 (parents
auto-created; overrides --out-dir).
gflow video i2v --initial-frame ./hero.png "Slow camera arc, soft golden light"
gflow video i2v --initial-frame ./first.png --end-frame ./last.png "morph between scenes" --model veo-quality
# Reference an existing in-project asset by media UUID (picker-selected via its
# catalog display name + exact-UUID tile since v0.58.0/#529; no re-upload):
gflow video i2v --initial-frame d6f1927a-3eae-4626-bc90-9a6ea7637bab "pan left" --project f6caf027-...
# Back-compat positional form:
gflow video i2v ./hero.png "Slow camera arc, soft golden light"
Exit codes specific to this surface: 7 — Chrome lost the upload's reply and the
readback could not prove the upload saved (RpcResponseUnavailableError; check the
project before re-running); 27 — Flow's upload endpoint rejected a
local frame file (MediaUploadRejectedError; try re-encoding, e.g.
ffmpeg -i in.jpg -q:v 2 -map_metadata -1 out.jpg, or reference the asset by
UUID instead); 9 — a frame UUID could not be located in the project's media
picker and had no integrity-verified local fallback to upload
(TransportTimeoutError; wrong --project, foreign UUID, or the recorded
local file is missing/changed — since v0.58.0 a reachable verified local file
rescues a picker miss instead of failing).
gflow video r2v¶
Reference-to-video (Flow "ingredients"): condition a generation on reference
images. Per-model cap: omni-flash ≤7, the veo-* models ≤3. Fires
batchAsyncGenerateVideoReferenceImages.
gflow video r2v PROMPT --ref IMG [--ref IMG ...] [--model] [--duration] [--resolution] [--count] [--aspect] [...]
Options:
--ref PATH Reference image; repeat for up to 7 (omni-flash) / 3 (veo). [required]
--resolution [360p|720p] Video resolution ('360p' or '720p', supported on omni-flash; on any other
model, or on the labs editor, the run stops before submit with exit 11
and no credits spent).
Omit for Flow's default.
--project ID Generate in this EXISTING Flow project instead of a scratch
project (see "Sharing one project across calls").
gflow video r2v "a knight in this armor walks forward" --ref armor.png
gflow video r2v "blend these worlds" --ref a.png --ref b.png --ref c.png --model omni-flash
On Flow's migrated
flow.google.comhost (#639) — a moved account, orGFLOW_CLI_FLOW_HOST=flow.google.com— only local--reffiles are served, with--project <id>. Each file is uploaded through the editor's own Upload entry under a run-unique name —hero.pnguploads ashero-a1b2c3d4.png(#792), so re-using the same reference across runs cannot bind an earlier copy — (the same path i2v uses, so it stays in the project's library like any upload) and then attached as an@mention in the prompt — references are not a chip slot on this host. A run whose references have not all attached is refused before submit (exit 32), and the app's own submit body must carry every uploaded media id with a reference-to-video model key (exit 7 otherwise) — the failure being a full-price clip with none of your references on it. References given by@Nameor--reference-entity(character entities) exit 36 there; an unmoved account keeps the labs driver for those.
--durationis refused on this path (exit 11). The host offers reference-to-video only at its base 8s tier. At 4s or 6s it does not refuse — it drops the references, types their uploaded names into the prompt and bills a text-to-video clip (measured at zero credits, 2026-09-06). Because the editor remembers the last duration used, an r2v run pins 8s itself rather than inheriting it. Pass no--duration, or--duration 8.
Sharing one project across calls¶
By default each video t2v / i2v / r2v call creates its own scratch Flow
project. Pass --project <id> (same contract as image t2i/i2i — see
gflow image t2i) to generate into an existing project
instead, e.g. to avoid one-throwaway-project-per-clip when scripting a
multi-clip storyboard:
gflow video t2v "establishing shot" --project PROJ123
gflow video t2v "close-up reaction" --project PROJ123
To make that the default, set GFLOW_CLI_PROJECT=<id> in the environment or
.env (#1039). image t2i/i2i,
video t2v/i2v/r2v, gflow run and the MCP generate tools then generate into that
project whenever no project is named, and print Using the default project <id>
(GFLOW_CLI_PROJECT). on stderr. --project (or "project" in a gflow run config)
and --project-name, which asks for a new named project, both win over it. It never
applies where --project names the project that owns existing media (video extend,
image/video upscale) or is required (scene, character, instructions), and
image batch / image upload, which have no --project, still create their own. A
GFLOW_CLI_PROJECT_NAME set in the environment names the pinned project rather than
asking for a new one. Project
ids belong to one Google account, so a pin the local catalog records only under another
profile is refused (exit 2) before a browser starts.
export GFLOW_CLI_PROJECT=$(gflow project create --name my-batch --json | jq -r .project_id)
gflow image t2i "a red bicycle" # lands in my-batch
gflow video t2v "a tram at dusk" # so does this
Batch video generation (shell loop)¶
gflow video has no batch subcommand — a manifest-driven video runner
was scaffolded early on but never worked and was removed. gflow image
batch (manifest-driven image generation) is unaffected and still ships. For
video, drive sequential generations through a plain shell loop instead.
Without --project, each gflow video t2v / i2v / r2v call opens its
own Flow project, so the resulting videos will NOT share a project_id
(unlike gflow image batch, which mounts one project across all prompts) —
but they DO get generated and downloaded. Pass the same --project <id> to
every call in the loop (see "Sharing one project across calls" above) if you
want them to land in one project instead:
# bash / WSL / macOS — one prompt per line
while IFS= read -r prompt; do
gflow video t2v "$prompt" --aspect 9:16
done < prompts.txt
# PowerShell — one prompt per line
Get-Content prompts.txt | ForEach-Object {
gflow video t2v $_ --aspect 9:16
}
The trade-off vs. a manifest runner: separate project_ids mean each
generation re-mints a reCAPTCHA (a few extra seconds per shot) and the
videos won't appear together in your Flow gallery. The same pattern works
for gflow video i2v <image> "<prompt>" and
gflow video r2v "<prompt>" --ref <img>.
Making a video longer than 8 seconds — which command?¶
A single Veo generation caps at 8 seconds. Four commands can get you past that and they are not interchangeable:
| You have / want | Use | Costs |
|---|---|---|
| Clips you already generated, want one file | gflow scene create --output |
Free — server-side concat |
| One clip, want more of the same shot | gflow video extend |
Credits per 8s segment |
| Several distinct shots with visual continuity | gflow video chain |
Credits per link |
| A scripted multi-scene piece | gflow movie |
Credits per clip |
The distinction that matters most: extend continues a shot, chain cuts to
a new one. Extend is seeded server-side from the source clip, so motion and
audio carry across the join; chain extracts the last frame locally and restarts
from a still, which is why it carries a fade-to-black guard. Use extend when a
cut would break the effect (a drone move, an establishing shot, footage timed to
a narration beat); use chain when a cut is what you want.
If you only need the clips joined and already have them, scene costs nothing —
reach for it before spending anything.
gflow video extend¶
Where the two ids come from.
MEDIA_IDis the clip you want to continue — list yours withgflow data list videos(themedia_idcolumn). The project id is the project that owns it:gflow data list projects, or copy it out of the Flow URL (/project/<project-id>).--projectis required here, unlike on the other generate commands, because extend has to find the workflow that ownsMEDIA_IDbefore it can create the scene to extend into.On flow.google.com: one segment per call. gflow opens the clip's editor (
/project/<id>/edit/<media-id>) and drives Flow's own Extend control; the page builds and signs the request itself. Before any click, Flow's project read must listMEDIA_IDas a video in--project(exit 11 otherwise), and every scene/extend request is checked at the route and aborted, before Flow sees it, if it names another clip. The new segment is downloaded to-o(default: the output directory). Not ported to flow.google.com yet, each refused with exit 36 before anything is billed: more than one PROMPT or-n > 1,--scene/--resume-from,--seed, and an--aspectother than the source clip's (leave--aspectoff; the extension keeps the source's). Each run leaves a new "Untitled Scene" in the project, and the plan shows no price because that host does not report one before submitting — comparegflow credits userbefore and after. To go further, extend the new clip again. "Cost and safety" and "What it produces" below describe the labs.google arm.
Continue an existing clip by another 8 seconds, then optionally render the whole thing to one file.
# one continuation
gflow video extend <media-id> "the wave recedes back into the ocean" --project <id>
# four continuations, different beats, rendered to a single mp4
gflow video extend <media-id> "the camera drifts upward" "the coastline opens out below" -n 4 -o long.mp4 --project <id>
MEDIA_ID is the clip to continue; each PROMPT describes one 8-second
segment. With --segments/-n greater than the number of prompts, the last
prompt is reused — so -n 4 with one prompt continues the same idea four times.
Overshooting is safe. Generate more than you need and trim the tail; that costs credits but never quality.
⚠️ A segment carries ~7 seconds of real content, not 8. Flow advertises 8s and bills for 8s, but the returned media measures 7.000s. When several segments are concatenated, each internal seam is preceded by ~1 second of frozen frame and silence as the shorter clip is padded into its 8s slot. A single-segment extend is unaffected. See KNOWN_ISSUES — render without
-oand trim in post if the seam matters.
What it produces¶
A Flow Scene containing the original clip plus each continuation — not a
single file. Pass -o/--output to render it to one mp4 through Flow's
server-side concat, which is credit-free. Without -o, render later with
gflow scene create --output <path>.
Cost and safety¶
- The exact credit cost is shown before anything is submitted, and a pre-flight balance check refuses a run your balance cannot finish.
--dry-runprints the plan without opening a browser or spending.- Segments submit one at a time, with a random pause between them
(
--jitter, defaulting toGFLOW_CLI_JITTER_RANGE). Generation itself takes ~2 minutes per segment, so a chained run is naturally paced — see ACCOUNT_SAFETY. - A refusal aborts and keeps the segments already generated; nothing is auto-retried. Re-running into a block only raises the profile's score.
- Ctrl+C reports what was spent and the scene to resume from.
Flags¶
| Flag | Meaning |
|---|---|
-n, --segments N |
How many 8s continuations (1–30). Default: one per prompt |
-o, --output PATH |
labs.google: render the whole scene (source clip + extensions) to one mp4 (free). flow.google.com: save only the new extension clip — scene concat is not ported there yet |
--aspect 9:16\|16:9 |
Portrait or landscape. No square — Flow has no square extend model. Default 9:16 on labs.google; flow.google.com keeps the source clip's aspect |
--project ID |
Required — the project owning MEDIA_ID |
--scene ID |
Extend inside an existing scene instead of creating one |
--resume-from ID |
Continue an interrupted run's scene — appends after the clips already there |
--seed N |
Fixed seed, for a reproducible run |
--jitter S |
Max seconds of pause between submissions |
--dry-run / --yes |
Print the plan and stop / skip the confirmation |
Model selection¶
You do not pick the model. Flow's extend family is tier-gated — the _ultra
variants are Advanced-only and unavailable elsewhere — so gflow reads your
account's capability listing and picks the cheapest model it can actually order,
which is what Flow's own UI does. The chosen key is recorded in the
extend_model_resolved log event.
gflow video chain¶
Render a JSONL manifest of links into one continuous last-frame I2V chain. Link 0 is a text-to-video (T2V) generation; every later link is an image-to-video (I2V) generation seeded by the extracted last frame of the previous clip, giving visual continuity with no server-side stitching.
⚠️ Each link is a pending video generation operation. A chain submits N sequential Veo generations that may consume credits; current cost varies by model, duration, account tier, and Flow policy — check Google Flow before submitting. An accepted operation may consume credits even if later polling or download fails. Run
--dry-runfirst to print the plan without submitting anything. The confirmation prompt is shown unless you pass-y/--yes.Requires the
chainextra. The last-frame extractor decodes the previous clip with PyAV (no system ffmpeg needed) and writes the seed JPEG with Pillow; the extra carries both. Without it,gflow video chainfails up front with exit20(FrameExtractionError) before reading the manifest or prompting for cost — ongflow-cli≤ 0.74.0 it was a generic exit1instead, and Pillow had to be installed by hand (see KNOWN_ISSUES.md). Install it with:
gflow video chain MANIFEST [OPTIONS]
Arguments:
MANIFEST JSONL manifest, one link per line. [required]
Options:
--model [veo-lite|veo-fast|veo-quality|veo-lite-lp]
Veo 3.1 model for every link. [default: veo-lite]
--max-links INTEGER Cap link count; error (exit 11) if the manifest
has more links than this.
-y, --yes Skip the per-credit cost confirmation prompt.
--dry-run Resolve the manifest, print the plan + credit cost,
and spend nothing.
--resume-from CHAIN_ID Resume a prior chain by its id; already-paid links
are skipped (not re-billed).
--jitter FLOAT Random 0..JITTER second pause between links
(anti-bot cadence). [default: 0.0]
--seed-offset INTEGER Extract the seed frame this many ms before EOF
(fade-to-black guard). [default: 0]
--aspect [9:16|16:9] Uniform aspect for every link. [default: 9:16]
--profile NAME Profile name (overrides default).
--out-dir DIR Directory for the link mp4s + seed frames.
--json Emit a machine-readable JSON result.
omni-flashis rejected for chains. Its single-clip start-frame i2v is wire-verified (2026-08-03, issue #125), but a chain renders N seeded links back-to-back and that has not been verified at chain scale — sochainstays on the Veo 3.1 family pending a chain-scale verification. The chain also aborts a link loudly (rather than reporting a fake success) if a generation is observed routing to the text-only endpoint — see KNOWN_ISSUES.
JSONL manifest format¶
One JSON object per line. Only prompt is required; model, aspect, and
duration are optional per-link overrides (omit to inherit the chain default).
Blank lines and #-prefixed comment lines are skipped.
durationin a chain: per-linkdurationaccepts4,6, or8seconds for Veo models, while10and non-standard durations are rejected during pre-flight before the first link is submitted (issue #634). Successful application of explicit duration depends on whether the active Flow account/cohort renders the duration control. A per-link"model": "omni-flash"override is rejected up front because omni-flash is not supported at chain scale (refs #125).
{"prompt": "a lone wolf on a snowy ridge at dawn, cinematic", "model": "veo-lite", "aspect": "16:9"}
{"prompt": "it lifts its head and turns to face the camera"}
{"prompt": "it bounds down the slope toward the valley"}
The chain enforces a uniform aspect across links for continuity, so the
chain-level --aspect is applied to every link (a per-link aspect override in
the manifest is currently informational).
Output — N clips, not one file¶
Each link is saved as its own mp4 (plus a linkN_lastframe.jpg seed frame
between links) under --out-dir (or the default output dir). Stitching the
clips into a single video is a separate step — use gflow scene to
concatenate them server-side. Chain does not auto-concat: see the
deferred auto-concat entry in
KNOWN_ISSUES for why.
Examples:
# Preview the pending video operation plan — submits nothing
gflow video chain story.jsonl --dry-run
# Generate the chain (one pending video operation per link), no confirmation prompt
gflow video chain story.jsonl --model veo-fast --aspect 16:9 --yes
# Resume a chain that died partway — already-completed links are skipped
gflow video chain story.jsonl --resume-from 1f2e3d4c-...
# Seed 150 ms before EOF to dodge a fade-to-black final frame
gflow video chain story.jsonl --seed-offset 150 --yes
gflow instructions¶
Manage a project's Agent Instruction cards — the brief Flow's agent consults when it generates. Enabled cards steer output (style, references) on agentic-cohort sessions. Setting up cards is credits-free. Full guide: INSTRUCTIONS.md.
Shipped (v0.28.0): the ephemeral -i / --instruction "text" flag on gflow image t2i /
i2i adds a one-off enabled text card for that generation.
Persistent cards: the gflow instructions add/list/enable/disable/rm/apply/toggle-mode
CRUD group manages a project's brief, with a single generic --ref per card that accepts an
image path, a generated-image UUID, or a character id/name (image → imageReferenceMediaIds,
character → characterReferenceEntityNames). Cards are selected by title (case-insensitive)
by default, or by the stable server --id for the ambiguous-title / scripting case (mirrors
gflow character's --name / --entity-id). Every subcommand requires --project. See
INSTRUCTIONS.md for the full reference.
gflow instructions add TITLE --text TEXT [--ref REF]... --project ID [--disabled]
gflow instructions list --project ID [--json]
gflow instructions enable (TITLE | --id ID) --project ID
gflow instructions disable (TITLE | --id ID) --project ID
gflow instructions rm (TITLE | --id ID) --project ID
gflow instructions apply FILE --project ID # declarative full-sync (TOML/JSON)
gflow instructions toggle-mode (--on | --off) --project ID
gflow tools¶
Discover and run prompt tools — named, single-purpose transforms applied to a prompt
before generation. The first built-in is creative-director (the Gemini "Creative Director").
Apply a tool inline on any generation command with -t/--tool (see prompt tools),
or use this group standalone. Full reference: TOOLS.md · PROMPT_EXPANSION.md.
gflow tools list [--json]
gflow tools show NAME
gflow tools run NAME "INPUT" [--style MODE] [--json]
list— registered tools (name, title, category, description). Includes your own "My Tools" TOMLs from<GFLOW_CLI_HOME>/tools/*.toml.show NAME— full spec incl. required env and available--stylemodes.run NAME "INPUT"— run the tool standalone (no generation, no credits); pipeable. Emits{name, original, expanded, was_expanded}with--json.
gflow tools list
gflow tools show creative-director
# Preview an expansion (needs GFLOW_CLI_LLM_API_KEY or _BASE_URL); never fatal
gflow tools run creative-director "a cat on a couch" --style cinema --json
To author your own tool, drop a TOML in <GFLOW_CLI_HOME>/tools/ — see
TOOLS.md § My Tools.
gflow scene¶
Compose ordered, trimmable video clips into a Flow Scene (the "Add Clip"
timeline). This is a credit-free, reCAPTCHA-free REST path: it arranges
existing clips (e.g. the per-link mp4s from gflow video chain) — it does not
generate anything. Each clip is referenced by its workflow id, optionally
trimmed with a start-end window in seconds.
gflow scene create¶
gflow scene create CLIP_REFS... [OPTIONS]
Arguments:
CLIP_REFS... One or more clips, in order. Each is
`workflowId[:start-end]` (trim in seconds, start<end).
[required]
Options:
--project PROJECT_ID Flow project id. [required]
-o, --output PATH Render the composed scene into ONE extended .mp4 at
PATH (server-side, credit-free, no local ffmpeg).
Without it, only the Flow scene is composed (no file).
--force Overwrite --output if it already exists.
--profile NAME Profile name (overrides default).
With --output, gflow calls Flow's server-side runVideoFxConcatenation to
stitch the clips into a single extended video. The render is credit-free and
needs no local ffmpeg — Flow returns the encoded video inline (base64) and
gflow writes it to PATH (a .mp4 suffix is enforced; takes up to ~3 min). The
compose is persisted to the local catalog before the render, so a render
failure is recoverable without re-composing. A non-.mp4 --output is
rewritten to .mp4; an existing file errors unless --force is passed.
Examples:
# Compose three chain clips into a scene (no local file)
gflow scene create wf-aaa wf-bbb wf-ccc --project proj-123
# Same, but also render one extended .mp4 (credit-free, server-side)
gflow scene create wf-aaa wf-bbb wf-ccc --project proj-123 --output story.mp4
# Trim the middle clip to its 2.0–6.5s window before concatenating
gflow scene create wf-aaa wf-bbb:2.0-6.5 wf-ccc --project proj-123 -o story.mp4 --force
gflow scene show¶
gflow scene show --scene SCENE_ID --project PROJECT_ID [--profile NAME]
Options:
--scene SCENE_ID Scene id to read back. [required]
--project PROJECT_ID Flow project id. [required]
--profile NAME Profile name (overrides default).
Reads a scene back and prints each clip's position, workflow id, trim window, source duration, and the composed total duration.
gflow character¶
Create and manage reusable Flow Character entities — project-scoped,
broadly-available identities you can reuse across generations (#145). Full
design + wire protocol: CHARACTER. Every subcommand takes
--project <id> (required); use --id/--name flags, never a positional.
gflow character create¶
⚠️ Costs credits.
createruns two paid image generations: a face reference (slot 0), then a front/side/back triptych body (slot 1) seeded by that face. It runs as a persist-before-spend, crash-recoverable saga — the entity id and workflow rows are recorded before the credited generation, so a crashed run is recoverable. Generation requires a Chrome-strategy profile.
gflow character create [OPTIONS]
Options:
--project PROJECT_ID Flow project id. [required]
--name NAME Display name for the new character. [required]
--face-prompt TEXT Prompt for the face reference image. [required]
--body-prompt TEXT Body/outfit DESCRIPTION (not a full prompt). gflow
wraps it in a self-contained front/side/back triptych
instruction seeded by the generated face, so one
generation yields all three angles. Omit to skip the
body step.
--voice NAME Preset voice (e.g. Charon); case-insensitive,
validated against `gflow character voices`.
--personality TEXT Personality notes for the character.
--model [nano2|nanopro] nano2 = Nano Banana 2 (default);
nanopro = Nano Banana Pro. [default: nano2]
--profile NAME Profile name (overrides default).
--locale TEXT BCP-47 locale. [default: en-US]
--json Emit a machine-readable JSON result.
There is no aspect-ratio control for characters. Generated images are
downloaded to local (or cloud) storage; the signed fifeUrl is used only at
download time and never persisted. The result reports the entity id, the bound
workflow ids, and each saved slot's local path (face, body).
Running it twice with the same --name creates two characters, not one. The
command is not idempotent on name: each successful run mints a fresh entity and spends
face (and body) image quota again. Check with gflow character list before re-running,
and remove a duplicate with gflow character rm. What is guaranteed is resume after
an interrupted run: if a create is killed mid-saga, the next run with the same name
picks up the recorded entity instead of minting a second one, so a crash costs no extra
quota.
gflow character list¶
Lists every Character entity in a project (name, entity id, voice, reference count).
gflow character show¶
Shows one character's detail (entity id, project, voice, personality, reference
workflow ids). Provide exactly one of --id or --name. An ambiguous
--name (multiple characters share it) exits with code 11.
gflow character rm¶
gflow character rm --project PROJECT_ID (--id ENTITY_ID | --name NAME) [--yes] [--json] [--profile NAME]
Deletes a Character by --id or --name. Provide exactly one of --id or
--name. Prompts for confirmation unless --yes (or --json) is supplied; an
ambiguous --name (multiple characters share it) exits with code 11.
FREE — no reCAPTCHA, no credit (POST flow:batchDeleteAssets).
gflow character voices¶
Lists the 29 preset Gemini voices available for character TTS — each with a
name, description, and sample URL. These names are what --voice validates
against (case-insensitively) on gflow character create.
gflow data list¶
Read-only browse over the local SQLite catalog. Shipped in v0.9.0.
gflow data list projects [--profile NAME] [--limit N] [--offset N] [--json]
gflow data list images [--profile NAME] [--limit N] [--offset N] [--json] [--all-copies]
gflow data list videos [--profile NAME] [--limit N] [--offset N] [--json] [--all-copies]
gflow data list profiles [--limit N] [--offset N] [--json]
gflow data list errors [--profile NAME] [--limit N] [--offset N] [--json]
Options:
--profile NAME Filter to one profile (not available on `profiles`).
--limit N Max rows returned. Range 1..1000. Default 20.
--offset N Rows to skip (pagination). Default 0.
--json Force JSONL output (one record per line).
--all-copies Show every local file copy separately (images/videos only).
By default, images and videos aggregate rows by Flow media ID. If an asset
has multiple local copies (e.g. re-downloaded to different paths), they appear
as a single row with a COPIES count and the path of the latest copy. Use
--all-copies to see every path as a separate row.
A project's title is the one gflow recorded when it first saw the project: the real title
for one gflow project create made, but a placeholder (gflow-cli video, or empty for an image
run) for one first met through a generation. gflow project create / rename replace it. A generation into an existing --project
never rewrites it (#1022).
The catalog does not import titles set on the website, so a project renamed there keeps
its old title here.
Output:
- TTY stdout → Rich-formatted table.
- Pipe / non-TTY / --json → JSONL.
Default sort: newest first (by created_at). Exit codes: 0 success (including the empty-catalog case — a missing/freshly-created DB is auto-migrated and returns 0 with no rows) / 2 Click usage / 16 DataStoreError family (migration mismatch, permission denied, corrupt schema). See #88.
Examples:
# Newest 20 projects across all profiles
gflow data list projects
# All images for one profile, aggregated by asset by default
gflow data list images --profile ffroliva --limit 50 --offset 0
# Show every local file copy separately
gflow data list images --all-copies
# Videos as JSONL for piping into jq
gflow data list videos --json | jq '.media_id'
# Profiles with at least one recorded generation
gflow data list profiles
# Failed generations, newest first (v0.39.0+, #341): started_at, command, mode,
# model, profile, error_type (waf-rejection, content-policy, ...), redacted detail
gflow data list errors --profile my-profile --json | jq '{started_at, error_type}'
errors lists terminal status="failed" operation rows — every paid
generation that raised (WAF 403, content policy, cohort pin, timeout, auth)
is recorded before the CLI exits, so block onset/recovery windows are
measurable. error_detail is redacted before persistence (no tokens, cookies,
or signed URLs). See
DATA_LAYER.md § Failure recording.
data list profilesvsgflow auth list—data list profilesshows profiles that have recorded generations in the catalog;gflow auth listshows profiles that have ever logged in viagflow auth login. A profile that logged in but never generated anything will appear inauth listbut not indata list profiles.
For full schema details and JOIN semantics, see docs/DATA_LAYER.md § Querying the data layer.
gflow data prune¶
Remove local_files database entries whose local paths no longer exist on
disk. Shipped in v0.9.1.
Useful after test runs that wrote to temporary directories, or after manually
deleting media files. Only local files (where storage_provider is NULL)
are scanned; cloud-stored assets are ignored to prevent accidental pruning of
remote objects.
Options: --dry-run Preview dead rows without deleting. --profile NAME Limit scan to a specific profile.
gflow data download¶
Fetch an already-generated video from Flow by its media ID, for the case where the generation succeeded but its download did not.
Video only. The signed URL this needs comes from a record Flow emits when a clip's own route loads, and an image's route does not carry one. An image media ID is refused immediately with exit 11 and a message saying so — it does not open a browser or wait. Tracked in #877.
That case is real and costs money: on flow.google.com a generation whose signed media
URL is not observed within the grace window exits 7 (WireFormatError) after the
credit has been spent. The clip is in the Flow project, the catalog has a row for it,
and before this command there was no way to fetch it — the only recovery the CLI offered
was to run the generation again and pay a second time.
Recovering costs nothing. The asset was already billed.
The transfer retries a dropped connection by itself — three attempts, sub-second
backoff, all inside the same timeout rather than multiplying it. A reset that survives
all three exits 6 (NetworkError, retryable) and says what to do, instead of the
bare Unexpected error it used to raise
(#895). Re-running is safe and still
costs nothing: each attempt asks Flow for a fresh signed link.
An image whose download fails is still recorded
(#1035). Flow generates the image,
and spends that model's daily quota, before gflow writes it. When the write then fails
(an --out directory it cannot write to, a locked file, a dropped transfer), image t2i,
image i2i, image batch and gflow run keep the image's media id in the catalog with no
local copy, under one FAILED operation in its project, and t2i / i2i print the media
id(s) and the project URL on stderr (also under --json). The exit code is the error's own:
a write refused by the OS is exit 11 ("Could not write the output file", with a hint to point
--out somewhere writable) since #1091;
it was exit 1 before. To get the file, open the project in
Flow; gflow data list images shows the row with copy_count 0. A download interrupted
with Ctrl-C leaves that operation STARTED, which gflow doctor reports as stuck after 24 h.
gflow data download MEDIA_ID [--out DIR] [--profile NAME] [--json]
Arguments:
MEDIA_ID Flow media UUID of the asset to fetch. [required]
Options:
--out DIR Directory to write into.
Default: $GFLOW_CLI_OUTPUT_DIR.
--profile NAME Scope the catalog lookup to a specific profile.
Default: search all profiles.
--json Emit a JSON summary instead of a table.
The command opens the clip's own route in Flow, takes the signed URL Flow reports for it, and verifies the bytes against the size Flow records before writing. That size check is not belt-and-braces: the same asset is also served as 360p and 720p re-encodes that carry valid MP4 magic bytes, so a magic-byte check alone would happily save the wrong file. A mismatch is an error, never a warning.
On success the local_files row is written too, so gflow data list videos stops
reporting copy_count: 0 for an asset that is now on disk.
Example:
$ gflow data download 9ad33c78-5762-4cbc-bcbe-07a4c3b061c7
gflow data download
┌────────────┬──────────────────────────────────────┐
│ media_id │ 9ad33c78-5762-4cbc-bcbe-07a4c3b061c7 │
│ profile │ ffroliva │
│ project_id │ 339f65ee-fec3-436e-bdd1-fa2acdbf4afd │
│ path │ ./out/9ad33c78-....mp4 │
│ bytes │ 2702168 │
└────────────┴──────────────────────────────────────┘
Exits 16 when the catalog has no row for the media ID (or the ID exists under several
profiles — pass --profile to disambiguate), and 7 when Flow does not report a usable
media URL for it, which is also what you get for a clip that has been moved to trash.
gflow data media¶
Look up a recorded operation by its Flow media ID. Prints a summary of the stored provenance record: profile, media ID, Flow project ID, kind (image/video), and the local paths or cloud URIs that were written for that operation.
Without --profile the lookup spans every profile in the catalog — matching the cross-profile default of gflow data list. Pass --profile NAME to scope to a specific profile (this is the way to disambiguate the rare case where two profiles share the same Flow media_id; the command refuses to guess and lists the candidates annotated with kind). See #87.
gflow data media MEDIA_ID [--profile NAME]
Arguments:
MEDIA_ID Flow media UUID (e.g. ddb6ef97-262d-49f4-8269-4a28c0fae6a2). [required]
Options:
--profile NAME Scope the lookup to a specific profile.
Default: search all profiles.
Example output:
Profile: default
Media ID: ddb6ef97-262d-49f4-8269-4a28c0fae6a2
Project ID: f1a2b3c4-0000-0000-0000-000000000001
Kind: image
Paths:
/home/user/Downloads/gflow-cli/images/2026-05-24/ddb6ef97_1.png
/home/user/Downloads/gflow-cli/images/2026-05-24/ddb6ef97_2.png
When GFLOW_CLI_STORAGE_URI was active for the run, the
same command prints cloud_uri_1, cloud_uri_2, and so on instead of
local_path_N rows.
Exit codes: 0 success, 2 media ID not found in the local database, 16 database error (see exit code table below).
gflow data sync¶
Reconcile the local catalog's display names against Flow's own project-listing
endpoint (flow.projectInitialData) — the source of the captions the media
picker searches by. Credit-free: one listing GET per project (~0.5s,
session-cookie auth); no generation surface is touched. Writes by
default — pass --dry-run to preview.
gflow data sync --names [OPTIONS]
Options:
--names Sync display names + presence (required; the only
sync mode, explicit by design).
--project ID Limit the sweep to specific Flow project id(s).
Repeatable.
--limit N Visit at most N nameless projects (newest first).
--since DATETIME Only consider rows created at/after this time
(e.g. 2026-08-01 or 2026-08-01T12:00:00).
--all Explicit full sweep of all nameless projects
(this is also the default scope).
--max-projects N Hard cap on projects visited per run. [default: 50]
--dry-run Fetch listings and preview what would be written
(no DB writes). Without it, sync WRITES by default.
--json Emit a JSON summary instead of text.
--profile NAME Profile whose catalog rows to sync.
What it fixes. Rows recorded before the caption existed (Flow computes
display names asynchronously server-side), or recorded by a pre-0.58.0
version, have no stored name — so --ref <uuid> runs fall back to a duplicate
re-upload instead of a picker selection. One sync pass restores the names and
the picker path works again.
Scoping. The default sweep visits every project that still has nameless,
non-ghost rows, newest first. --project restricts to explicit project ids,
--limit caps how many nameless projects are considered, --since drops rows
created before the cutoff, and --max-projects is the hard visit cap on top
of whichever scope applies.
Multiple profiles. Sync operates on one profile per run — the
resolved default, or the one named with --profile. With several profiles,
run it once per profile. gflow doctor shows the per-profile split in its
catalog counts (e.g. 12 asset(s) have no display name (my-profile: 9, work: 3))
so you can see which profiles still need a pass.
Ghost marking. When a listing is provably complete (no pagination
markers) and contains at least one media item (a mass-tombstone guard
against a Flow shape drift hiding the whole media[] array), cataloged rows
whose media no longer exists remotely are flagged
sync.status = "missing_remote" — a tombstone, never a deletion: the
row and its provenance stay queryable, and tombstoned rows are skipped by
later sweeps. On a partial listing absence proves nothing, so nothing is
ghost-marked.
Un-ghosting. A tombstoned row whose media reappears in a later listing has its tombstone cleared automatically; if still nameless it rejoins the next sweep.
Privacy gate. Display names are prompt-derived captions, so under
GFLOW_CLI_HISTORY_PROMPTS=redacted sync refuses up front (exit 11) with a
remediation naming the env var — nothing is fetched or written.
Exit codes. 0 success (including a no-op run); 34 partial failure —
some projects failed, at least one succeeded (SyncPartialError, retryable:
completed writes stay committed and a re-run resumes where it left off); 11
redacted-mode refusal; 2 usage error (e.g. missing --names).
# Preview the full sweep without writing
gflow data sync --names --dry-run
# Restore names for one project
gflow data sync --names --project 6e4460fb-e955-4a44-806c-9b34d4998c9f
# Bounded background-friendly sweep, machine-readable summary
gflow data sync --names --limit 10 --json
Sync is idempotent: a second run over an already-reconciled catalog visits nothing, writes nothing, and leaves every row's metadata byte-identical — safe to schedule or re-run after a partial failure.
gflow doctor¶
Read-only pre-flight diagnostics over the local catalog, database, and
environment. Doctor diagnoses, never heals: nothing is migrated, repaired,
or written — not even pending schema migrations. All database access is
strictly read-only (SQLite may create transient -wal/-shm sidecar files
during the read-only open; the database itself is never modified), so it is
safe to run against a live catalog at any time.
gflow doctor [--json]
Options:
--json Emit a machine-readable JSON report
(experimental; shape may change).
Eleven checks run on every invocation:
| Check id | Detects | Suggested remediation |
|---|---|---|
catalog.display_name_missing |
Assets with no recorded display name (the picker search key) | gflow data sync --names |
catalog.local_file_missing |
Cataloged local files that no longer exist on disk | gflow data prune --dry-run |
catalog.sha256_null |
Local files with no recorded sha256 | gflow data sync |
db.migration_drift |
Schema does not match the packaged migrations, or was written by a newer gflow-cli | gflow data sync / gflow update |
db.wal_state |
Stale -wal/-shm sidecars next to a non-WAL database; PRAGMA quick_check anomalies |
Close other gflow processes and re-run; restore from backup on quick_check failures |
operations.stuck_started |
Operations in started for over 24h with no completion |
gflow data errors prune |
queue.stuck_processing |
Queue tasks claimed as processing for over 24h |
gflow data prune --dry-run |
env.deprecated_vars |
Deprecated env vars still set (GFLOW_CLI_PREFER_CLASSIC, GFLOW_CLI_FORCE_AGENT_UI, GEMINI_API_KEY), or GFLOW_CLI_DB_PATH disagreeing with the resolved settings path |
Unset the variable / switch to its successor |
env.browsers_missing |
Playwright Chromium is not installed | playwright install chromium |
auth.files_present |
No auth profiles, or profiles without saved cookies | gflow auth login |
env.browser_proxy |
Reports whether GFLOW_CLI_BROWSER_PROXY is set (scheme://host:port, never credentials); informational, never changes the exit code |
Use the same proxy for HTTPS_PROXY and for signing in (CONFIGURATION) |
Severity model. Every check reports pass, info, warn, or fail.
info is worth knowing but not a defect — it never flips the overall status
or the exit code; only warn/fail do. In the brew-doctor spirit the report
itself says it best: findings are diagnostic signals, not a to-do list — if
everything is working as expected, there is no need to chase them.
Exit codes. 0 when every check passes (info findings included), 33
when any warn/fail finding is present — a successful diagnosis, not an error
class. Internal errors keep their standard typed codes (e.g. 16 when the
database cannot be opened at all — see the exit-code table below).
--json (experimental). Emits a machine-readable envelope whose contract
keys are overall_status (ok/issues) and checks[], each entry carrying
at least check (the id from the table above) and severity. The shape may
grow; treat unknown keys as forward-compatible.
Redacted history. Doctor output is redaction-safe: rows are identified by
UUID only (never display-name values), and paths are sanitized. Under
GFLOW_CLI_HISTORY_PROMPTS=redacted the catalog.display_name_missing check
reports info instead of warn — name backfill is deliberately suppressed by
that privacy setting, so missing names are expected, not a defect.
gflow update¶
Upgrade gflow-cli in place, through the package manager that installed it.
Stop every other gflow process first — see below.
The installer is read off the install itself, never guessed from PATH:
| Marker in the install's venv root | Installer | Command run |
|---|---|---|
uv-receipt.toml |
uv tool |
uv tool upgrade gflow-cli |
pipx_metadata.json |
pipx |
pipx upgrade gflow-cli |
| neither | plain venv | <that venv's python> -m pip install --upgrade gflow-cli |
gflow update first asks PyPI for the latest version (when PyPI answers, this
refreshes the once-a-day notice cache too). If you are already current it says so and runs
nothing. Otherwise it runs the manager with its output shown, then re-reads the
venv's Playwright version: when the upgrade moved it, a hint gives you the exact
<that venv's python> -m playwright install chromium so the browser build matches. If PyPI is
unreachable the manager still runs — it is the authority on what is installable.
--check only reports installed vs latest plus the command that would run;
--json returns {status: "ok", installed, latest, update_available, installer,
command, upgraded, notes} (latest / update_available are null when PyPI could not
be reached; after an upgrade latest is the version the venv actually reports;
notes carries the Playwright hint and the Windows launcher caveat below).
ConfigurationError (exit 11) cases:
- before anything is spawned — an editable / local-path / VCS / source
install (PEP 610
direct_url.jsonpresent): update those the way they were installed (git pull, reinstall from the checkout);uv/pipxdetected but not onPATH, or a plain venv with nopipmodule (auv venv): the message carries the exact command to run yourself; - after the manager ran — first, the venv can no longer import gflow-cli at
all (see below); then, the venv still reports the old version (a receipt
pinned to one version makes
uv tool upgradea silent no-op, for instance), or the version could not be re-read at all. The manager's own output is above the error. One carve-out: when PyPI was unreachable and the manager exited 0 and nothing changed, the manager simply found nothing newer — exit 0.
A half-replaced environment is detected, not reported as a version. Managers
replace files in place, so an interrupted run can leave a venv whose metadata
reads perfectly and whose imports are dead — measured on Windows in
#848, where an aborted native
dependency swap left every command dying on AttributeError: module 'greenlet'
has no attribute 'greenlet' while the version still read 0.69.0. After the
manager runs, gflow update therefore imports gflow-cli in a fresh isolated
interpreter before it looks at any version. If that import fails the command
exits 11 saying the install is unusable and quoting the interpreter's own last
line, and the remediation is a forced reinstall
(uv tool install "gflow-cli==<version>" --force --reinstall, or the pipx / pip
equivalent) — a plain upgrade cannot repair that state, because it reads the
metadata, finds it current and changes nothing.
The venv is the truth, not the manager's exit code: after the manager runs,
gflow update re-reads the installed gflow-cli version from a fresh interpreter
and reports that. Windows: the gflow.exe launcher you are running holds
its own file open, so it can be neither overwritten nor renamed while it runs.
Measured on uv tool upgrade: the new wheel installs, then uv exits 1 copying
the launcher ("os error 32"). The upgrade did happen — the launcher only
points at the venv's python — so gflow update reports it as upgraded with a
note quoting the manager's exit code; the next update from another shell
refreshes the launcher.
Stop every other gflow process BEFORE you upgrade — not after. The launcher
lock just above is the benign case: it is the gflow.exe you are running, and
the upgrade lands anyway. A different gflow, holding the install's contents
open, is not — there the manager can abort part-way and leave the environment
broken. The holder reported so far is gflow mcp run: an editor with gflow
registered as an MCP server starts one per session, and they outlive the command
that started them. Reported with a reproduction in
#848:
uv tool install --force failed with "Access is denied" on
…\uv\tools\gflow-cli\Scripts and left the install answering
No module named 'gflow_cli' until those processes were stopped and the install
re-run.
Close the editor sessions (or stop the servers) first. On Windows,
tasklist | findstr gflow lists image names only — a server registered as
uv run gflow … shows up as uv.exe, not gflow.exe; elsewhere
pgrep -fa gflow prints the command line it matched, so you can see what you
are about to stop. gflow update cannot run this check for you: it is itself
one of those processes. If an upgrade does abort anyway, the exit-11 forced
reinstall above is the repair — run it from a shell with no gflow running.
Restart any running gflow serve / MCP server afterwards too; a long-lived
process keeps the old code until it restarts. There is deliberately no MCP twin
of this command: a server must not replace its own code underneath itself.
Every command also prints an update banner on stderr (a one-line notice when stderr is not a terminal) when a newer release is known — see CONFIGURATION § GFLOW_CLI_UPDATE_CHECK for the once-a-day cache and how to silence it.
gflow models¶
Print the image and video model catalog — the source of truth for what
--model accepts on the generation commands. Defaults to a Rich table; pass
--json for a single machine-readable object.
Per model the catalog reports: name, label (image models: the name Flow's
model menu shows, read on English accounts, such as Nano Banana Pro; display
only), the CLI aliases the matching generation command's --model choice
actually accepts, ref_cap (max reference images),
plus default (image models) and max_duration (video models). Because it is
built from the same Model / VideoModel enums + alias maps the generation
commands use, any alias it prints is guaranteed to be accepted back by
--model — a UI populating its model picker from gflow models --json can
round-trip every value. MCP clients read the same object from
gflow_list_models (#883).
# Human-readable table
gflow models
# Machine-readable catalog for a worker / model picker
gflow models --json
JSON output (--json)¶
The generation commands (image t2i / image i2i, video t2v / i2v /
r2v), auth list, gflow models, and gflow docs accept --json for machine-to-machine
use. When set:
- The command emits one parseable JSON object (or array, for
auth list) on stdout and nothing else — progress chatter is suppressed and structured logs go to stderr, sojson.loads(stdout)always succeeds. image t2i/i2iemits the completeGeneratedImageresult (media_name,workflow_id,seed,prompt,model_name_type,aspect_ratio,dimensions,fife_url,is_signed_url) plus the on-disklocal_path;ref_countis included oni2i. Single-prompt only —--jsonrejects multi-prompt batches with a Click usage error.model_name_typeisnullwhen Flow served the account fromflow.google.com— that host's reply carries no model field, and gflow reports what it observed rather than echoing your--modelback (#789; on the agent-only composer too, #1012). It is alsonullin themodelcolumn ofgflow datafor those assets. Onlabs.googleit is Flow's own value, e.g."NARWHAL".video t2v/i2v/r2vemits theVideoResult(status,command,media_id,generation_status,succeeded,local_path,failure_reasons,error_message) plus the request echo (model,mode,aspect,duration,count,seed).- On failure, the command still emits a JSON payload — an RFC 9457
problem-details object including a
retryableflag (true for WAF / rate-limit / network / timeout) that a worker scheduler can key its retry-vs-absorb decision off — and exits with the same non-zero code as the Rich path.
The data-layer recorder fires regardless of --json, so audit history is
independent of the output channel.
gflow mcp¶
Model Context Protocol server for IDE/agent integration. Full reference: MCP.md.
gflow mcp run [--profile NAME] [--no-spend]— start the MCP server over stdio (Claude Desktop, Cursor, VS Code). Auto-selects your default profile; pin one with--profileorGFLOW_CLI_PROFILE.--no-spend(#496) never registers the two credit-spending tools (gflow_generate_image,gflow_generate_video), so a connected agent cannot even see them intools/list— read-only tools stay available.GFLOW_MCP_NO_SPEND=1does the same and also coversgflow serve(which takes the same flag). See CONFIGURATION § GFLOW_MCP_NO_SPEND.gflow mcp setup [--target claude-desktop|cursor|vscode]— write the gflow server entry into the target client's config. Non-destructive: existing content is merged, an existinggflow/gflow-clientry is left untouched ("Already configured"), a pre-existing file is backed up once as<name>.gflow-backup, and a corrupt config fails loud (exit 11) without being modified. See MCP.md § Setup Instructions.
For MCP over HTTP, see gflow serve and MCP.md.
gflow run¶
Sequential JSON-described batch image generation. New in v0.5.0a1.
gflow run --config FILE [--output-dir DIR] [--profile NAME] [--project ID] [--resume] [--continue-on-error|--fail-fast]
The config is a JSON file with a top-level prompts array; each entry
produces 1–4 images through one FlowApiClient session (one Playwright
browser, one Flow project; prompts run one after another).
By default each run creates a new Flow project titled gflow-cli run. Pass
--project ID (or set "project" in the config; the CLI wins) to generate every
row into an existing project instead, as gflow image t2i --project does: no
project is created, and a title already in the local catalog is kept (a project the
catalog has not seen yet is recorded without one, as with t2i --project).
Config schema¶
{
"profile": "<your-profile>",
"transport": "ui_automation",
"output_dir": "out/example-batch",
"prompts": [
{
"text": "a quiet mountain lake at dawn, cinematic photography",
"aspect_ratio": "9:16",
"model": "nano2",
"count": 1,
"output_filename": "lake_scene"
},
{
"text": "a sunlit forest path in autumn",
"aspect_ratio": "16:9"
}
]
}
| Key | Required | Default | Notes |
|---|---|---|---|
prompts |
yes | — | 1–50 entries. |
prompts[].text |
yes | — | 1–2000 chars. |
prompts[].aspect_ratio |
no | 9:16 |
9:16 / 16:9 / 1:1 / 4:3 / 3:4. |
prompts[].model |
no | nano2 |
Any image --model value: nano2, nano2.1, nano-2.1, nano-pro, nano2-lite, image4, imagen4. |
prompts[].count |
no | 1 |
1–4. |
prompts[].output_filename |
no | prompt_<index> |
Filename stem; saved as <stem>_<image-index>.png. |
prompts[].ref |
no | — | "batch:N" (generate from row N's image) or a local image file. See Referencing an earlier row and Referencing a local file. |
profile |
no | active profile | CLI --profile overrides. |
transport |
no | ui_automation |
Experimental strategies need GFLOW_CLI_EXPERIMENTAL_TRANSPORTS=1. |
output_dir |
no | out/<UTC-timestamp>/ |
CLI --output-dir overrides. |
project |
no | new gflow-cli run project |
Existing Flow project id (letters, digits, hyphens) to generate every row into. CLI --project overrides. |
Error semantics¶
--continue-on-error (default): one prompt failing logs the error and continues. Final exit code is the max per-prompt exit code (so a WafRejectionError anywhere in the batch makes the whole run exit 10).
--fail-fast: first failure stops the batch. Remaining prompts are reported as SKIPPED in the summary table.
A row skipped because its parent failed (see below) is not an error of its own: the run exits with the parent's code.
Referencing an earlier row¶
A row can generate from the image another row made: set "ref": "batch:N", where N is
that row's position in prompts (0-based).
{
"prompts": [
{"text": "a single red apple on a wooden table", "aspect_ratio": "1:1"},
{"text": "the same apple, now green", "aspect_ratio": "1:1", "ref": "batch:0"},
{"text": "the green apple on a blue plate", "aspect_ratio": "1:1", "ref": "batch:1"}
]
}
- Nothing is uploaded. Row N's image is already in the run's Flow project, so it is referenced where it is, by the handle Flow returned when it was generated. The project holds no duplicate.
- Order. Rows run in file order; a row that references a later row waits only until that row has run. Output names and the results table keep each row's own number.
- A referenced row must make one image (
"count": 1, the default), sobatch:Nnames exactly one image. Anything else is refused before the browser starts (exit 11), as are an out-of-range row, a row referencing itself, a cycle, and any form other thanbatch:<number>(no spaces, signs or leading zeros). - A failed parent. Its direct dependents are skipped with "parent row N failed", and theirs with "parent row M was skipped"; none is submitted without its reference. A parent whose image was generated but whose download failed still counts as generated: its children run.
- Tracking. Each row is recorded in the local catalog; a referencing row is recorded
as image-to-image with its parent as the input (
gflow data). - Resume with
--resume(#934). Re-run the same config into the same project (--projector"project"; without one, exit 11) and output folder. A row whose files (<stem>_<n>.<ext>, e.g.prompt_0_0.jpg) are all there and are recorded in the local catalog as images of that project is shown asRESUMEDand not submitted; itsbatch:Nchildren reference it in place. Every other row is generated. The default output folder is dated, so to resume on another day pass the same--output-dir(oroutput_dir). Without--resume, a re-run regenerates every row. batch:Nor a local file. A media id orreference_entityin a row is refused (exit 11); for those usegflow image i2i --refor--reference-entity.- A parent Flow returned without a caption cannot be found in the composer's
@picker, so its child is refused (exit 36, "an image Flow returned without a caption"). - Measured on flow.google.com (2026-10-01). An account served labs takes a different driver, which references the image by its media id; that arm has not been observed.
Referencing a local file¶
A row's ref can also be a local image: "ref": "refs/product.png".
- Resolved against the config file's folder (an absolute path also works), so a config and its images move together. The file must exist and be a real image (PNG, JPEG, WebP or GIF, up to 20 MB) before the browser starts; anything else is refused (exit 11).
- Uploaded once per run. The first row that names a file uploads it into the run's project; every row that names the same file references that upload in place. The project holds one copy.
- A failed upload fails that row with the reason; a later row naming the same file tries the upload again.
- Not resumable. A re-run uploads the file again, into its new project (or, with
--project, into that same project). - Measured on flow.google.com (2026-10-01). When Flow serves the project from labs,
the file goes through the REST upload instead; that path has not been observed live.
GFLOW_CLI_FLOW_HOST=labs.googleon a project Flow serves from flow.google.com is refused (exit 36), not rerouted.
Example¶
The bundled examples/sample_config.json produces three images at three aspect ratios in gflow-output/example-batch/. Copy and edit for your own scenes.
gflow movie¶
Sequential TOML-described multi-scene AI movie generation. New in v0.14.0.
gflow movie template¶
Writes a starter movie.toml template.
OUTPUT: The destination path for the template file (defaults to./movie.toml).--force: Overwrite the destination file if it already exists.
gflow movie run¶
Generates individual video clips sequentially from a single TOML manifest file, maintaining crash-recoverable state in a local state JSON file.
gflow movie run MANIFEST [--out-dir DIR] [--profile NAME] [--continue-on-error|--fail-fast] [--dry-run] [--stitch]
Refer to the complete Movie Manifests Guide for detailed syntax, character entity creation, and style consistency features.
Parameters¶
MANIFEST: Path to yourmovie.tomlmanifest file (required positional argument).--out-dir DIR: Output directory for generated video clips and the handoff file. Defaults toout/orGFLOW_CLI_OUTPUT_DIR.--profile NAME: Specify Google Flow profile to use.--continue-on-error: Skip failed scenes and continue generating subsequent scenes (default).--fail-fast: Terminate immediately on the first failed scene generation.--dry-run: Parse the manifest, estimate credits, and print the generation plan without calling Google Flow.--stitch: Run a post-generation preview stitch using ffmpeg to concatenate clips into a single video file.
Manifest structure (movie.toml)¶
title = "My Cinematic Film"
project = "Cinematics"
[style]
look = "3d animation"
mood = "mysterious"
negative = "text, watermark"
prefix = "Cinematic film still, high detail."
suffix = "Shot on IMAX 70mm, unreal engine 5 render."
[style.variants.warm]
suffix = "Golden hour light, warm color grading."
[[characters]]
name = "Stickman"
identity = "text"
voice = "alnilam"
[[scenes]]
id = "scene_01"
action = "A mysterious stickman walks slowly through a dark forest."
framing = "wide"
duration = 4
characters = ["Stickman"]
[[scenes]]
id = "scene_02"
action = "Close up of the stickman looking back in shock."
framing = "close-up"
duration = 4
characters = ["Stickman"]
style_variant = "warm"
Outputs¶
- Clip Files: Individual video files saved under
<out_dir>/scene_<id>.mp4(or other formats). - State File: A
<stem>-state.jsonfile recording completed operations, seeds, and metadata, allowing a run to resume without wasting credits. - Handoff Manifest: A
<stem>-handoff.jsonfile that projects the run's metadata and clip paths, designed for downstream assemblers.
Recipes¶
Burn through a directory of inputs¶
mkdir -p out
for img in ./inputs/*.png; do
name=$(basename "$img" .png)
gflow video i2v "$img" "Cinematic push-in" --out-dir out
done
New-Item -ItemType Directory -Force -Path out | Out-Null
Get-ChildItem ./inputs/*.png | ForEach-Object {
gflow video i2v $_.FullName "Cinematic push-in" --out-dir out
}
Fan out an image prompt 4-way¶
Run two profiles concurrently¶
# Terminal 1
gflow image batch ./batch-a.tsv --profile work
# Terminal 2 (different profile = different Chromium context = OK)
gflow image batch ./batch-b.tsv --profile personal
(Same profile concurrently → the second invocation fails fast with ProfileLockedError, exit code 11, before any Chrome process starts. Use different profiles or wait — or set GFLOW_CLI_LEASE_WAIT_SECONDS=N (v0.56.0, CONFIGURATION) to have the second invocation poll the lease and take over as soon as the holder finishes.)
JSON logs for piping into Loki/Datadog¶
Piping stderr into
jq? SetGFLOW_CLI_UPDATE_CHECK=0too — the update notice (v0.56.0; a banner on a terminal, one plain-text stderr line when piped) would break the parse the day a newer version publishes.
Exit codes¶
Phase 4 (v0.4.0a1+) maps every GFlowError subclass to a stable exit code so
shell scripts can branch on the failure mode without parsing stderr.
| Code | Error class | Meaning | Remediation |
|---|---|---|---|
0 |
— | Success | — |
1 |
unhandled / unmapped | Anything not derived from GFlowError, a typed error with no code of its own (e.g. a reCAPTCHA mint failure, type …/errors/recaptcha-mint, #915 — branch on the --json type) — or a deliberate CLI verdict: gflow auth status exits 1 for a dead/unverifiable session |
Re-run with --verbose; for auth status follow the printed hint; file a bug if it persists |
2 |
usage error (Click) | Bad usage / missing arg / profile missing | Standard CLI usage error |
3 |
AuthExpiredError |
Session cookies rejected by Flow (401/403), or Flow served one of its OAuth/sign-in routes instead of the page gflow asked for (#756) | gflow auth login --profile <name> — but read the error's own remediation_hint first. AisandboxAuthError shares this code. (Before v0.87.1, when gflow credits failed on an account migrated to flow.google.com, re-logging in could not help and could roll the profile's browser-strategy marker back (#795, #791).) Since v0.87.1 a credits exit 3 means a sign-in landing, fixable with gflow credits --refresh |
4 |
RateLimitError |
Quota / rate limit hit, exhausted retries | Wait + reduce GFLOW_CLI_CONCURRENCY |
5 |
ContentPolicyError |
Flow rejected the prompt (200 + empty media[]) |
Soften prompt wording |
6 |
NetworkError |
Network failure persisted across 3 attempts. A failed download of media Flow already made (MediaDownloadError) is retryable: false (#941): use gflow data download instead of re-running — including a signed-media download whose connection dropped on every attempt (#895), or whose signed URL answered an HTTP error such as an expired link (exit 7 until #941 part 2) |
Check connectivity — but read the error's own remediation_hint first. When a generation download fails this way the clip was already produced and billed: the hint names it, and gflow data download <media_id> recovers it for free. Re-generating pays twice |
7 |
WireFormatError |
Unexpected response shape — Flow API changed. Also RpcResponseUnavailableError (flow.google.com): Chrome lost the reply body of a project create, rename or upload (#1009), or a project create or rename got no reply within 120 s (#1037), and gflow could not confirm the outcome by reading the project back |
File a bug (do NOT include captured tokens or signed URLs). For RpcResponseUnavailableError the write may have saved: open the project and check its title or the uploaded file before re-running, or you may get a duplicate (see KNOWN_ISSUES) |
8 |
AuthMissingError |
Required auth credential is absent from profile | gflow auth login --profile <name> |
9 |
TransportTimeoutError |
Browser/API operation exceeded its timeout (incl. an i2v frame UUID not found in the media picker, #287; or a wedged submission stage — the error names the stage and your Playwright version) | Retry; raise the relevant timeout. If the detail says clicking submit, not terminal or unreadable, or --json shows retryable: false, the submit already went out (#1010): check the project in Flow before re-running, because a re-run bills again. A lost image reply is recovered from the project grid since v0.89.1 (#1075), so exit 9 there means the grid did not show exactly the requested images. For a frame-UUID miss verify the UUID belongs to the --project passed; for a stage_stalled abort check your Playwright is in range (see KNOWN_ISSUES) |
10 |
WafRejectionError |
Flow security layer rejected the request | Change prompt/request and retry |
11 |
ConfigurationError |
Local configuration or browser mode is invalid, including a malformed row in a gflow run config, an image batch manifest, a movie manifest or a video chain manifest (#935: image batch reported these as exit 2 before); a --project id Flow answers with its /404?reason=project page, meaning not found or not accessible to this account (#974, #1053); an output folder gflow cannot write to (--out, GFLOW_CLI_OUTPUT_DIR, GFLOW_CLI_STORAGE_URI), after a generation that succeeded: the asset is still in the Flow project (#1091; a generated video clip keeps exit 6); a cloud GFLOW_CLI_STORAGE_URI combined with -o, video chain or movie run --stitch, refused before billing (#1008) — on the migrated flow.google.com host also a request the host cannot take as given (no --project, a model its menu does not offer, a --duration its settings pane renders no control for); includes ProfileLockedError (same-profile lease contention: another gflow/daemon/MCP call already owns this profile; also a Chrome window you opened yourself holding the profile, #984 — close it or use another --profile), ProfileAccessError (Windows Chrome cannot write the profile directory; raised at the generation client's launch), and ProfileEngineDowngradeError (the profile was last written by a newer Chromium major than the bundled engine about to open it — see AUTHENTICATION § Chromium downgrade guard) |
Fix the option/env var shown in the error; for lease contention wait, use a different --profile, or set GFLOW_CLI_LEASE_WAIT_SECONDS=N to wait bounded; for profile access denial grant write access to the complete profile directory or use a writable profile; upgrade gflow-cli/Playwright or re-run gflow auth login for a downgrade refusal |
12 |
AuthLoginTimeoutError |
Browser sign-in was not completed in time | Re-run login or raise GFLOW_CLI_AUTH_LOGIN_TIMEOUT |
13 |
SecurityError |
Unsafe local profile or secret handling blocked | Follow the error's safety guidance |
14 |
AuthBrowserRejectedError |
Sign-in rejected the browser for navigator.webdriver |
Re-run gflow auth login; with Chrome installed the chrome strategy retries automatically |
15 |
BrowserSessionClosedError |
The automation browser window was closed mid-operation | Re-run; keep the browser window open until the command finishes |
16 |
DataStoreError |
Local database cannot be opened, a migration failed, or the DB schema is newer than the installed gflow-cli | See below |
17 |
ModelModeIncompatibilityError |
The chosen video model can't do the requested mode — today that is omni-flash for chain (issues #125, #626) |
Use a Veo 3.1 model (veo-lite / veo-fast / veo-quality / veo-lite-lp) for chain. Single-clip i2v with omni-flash, --end-frame included, is accepted |
18 |
VideoModelSelectionError |
gflow could not select the requested video model in Flow's editor for an i2v run (model-picker option not found) |
Usually transient — retry; if it persists, Flow's model-picker UI changed (report referencing #125) |
19 |
SceneConcatError |
Server-side scene render/concat failed (gflow scene --output) |
Retry; the recorded compose survives, so re-render is safe |
20 |
FrameExtractionError |
Could not extract the last frame for a video chain link | Check the source video downloaded intact; retry the link |
21 |
ChainPartialError |
A video chain stopped mid-way; earlier links completed | Resume from the last completed link shown in the error |
22 |
UpscaleUnavailableError |
4K image upscale is gated to Flow Ultra accounts; or requested video export quality is disabled | For images, use --scale 2k or upgrade the Flow plan; for videos, check available options in Flow |
23 |
UiSelectorDriftError |
A Flow editor control could not be located — Google changed the frontend (issues #183, #493) — or a blocking announcement overlay survived dismissal, so no control below it can be clicked (probe overlay_close_button, #593). On the migrated host a missing submit control is checked against the wallet first and reported as 37 when Flow swapped in its insufficient-credits warning, so a short balance no longer arrives here |
If the detail says the model picker is hidden, nothing was submitted: turn the Agent chip off in a browser and re-run (#788). Otherwise update gflow-cli; file a bug with the probe name + the diagnostics JSON / debug screenshot referenced in the error message, plus the incident bundle's report.md when one was written. For overlay_close_button: open the project once in Chrome and dismiss the announcement — the dismissal persists on your account |
24 |
BrowserEngineUnavailableError |
GFLOW_CLI_BROWSER_ENGINE=patchright but the engine is not installed |
pip install 'gflow-cli[patchright]', or unset GFLOW_CLI_BROWSER_ENGINE |
25 |
FlowAgentUiError |
The profile is on Flow's Agentic UI cohort and the classic media panel is unrecoverable for this operation — or, on flow.google.com's agent-only composer (#799), the agent did something other than what was asked: a second credit confirmation, more results than requested, or an image turn that made nothing (a video turn that makes nothing ends in exit 9, TransportTimeoutError, instead) |
On labs this is rare since v0.38.0 (#332): the mode controller reliably recovers agentic→classic, so first retry with --ui-mode classic. On the agent-only composer it is retryable: false — read detail (it may include the agent's last reply), rephrase the prompt, and check the project before re-running a video. See KNOWN_ISSUES |
26 |
MediaAttributionError |
Generated media could not be reliably attributed to this request (issue #281) | Re-run; a dedicated project with fewer pre-existing assets avoids the ambiguity |
27 |
MediaUploadRejectedError |
Flow's upload endpoint refused the input file (uploadImage 4xx, issue #287) |
Re-encode the image (ffmpeg -q:v 2 -map_metadata -1), or reference the asset by its media UUID |
28 |
UiModeUnavailableError |
The Flow UI arm this command required (--ui-mode/GFLOW_CLI_UI_MODE; -i forces agentic for images; labs video always requires classic — no agentic labs video driver exists; an agent-only flow.google.com composer (#799) is routed before this policy and never exits 28) couldn't be reached after a switch attempt; aborted before submitting — no credits spent (issue #299) |
Retry (the cohort flaps per load); try another --profile; for images you can also relax GFLOW_CLI_UI_MODE — for video there is nothing to relax |
29 |
MentionIndexUnavailableError |
An @mention was present but the catalog source needed to resolve it (character entities or media assets) failed to load — distinct from an empty index, which is not an error. Also raised on a page served labs for an image character known only by name (#723), since that picker attaches by entity id |
Check network connectivity (character source) or GFLOW_CLI_DB_PATH / filesystem permissions (media source), then retry. For the by-name character on labs, pass --reference-entity <id> |
30 |
QueueSchemaError |
A gflow serve/MCP worker-queue task payload has an unrecognized schema_version or fails validation against the typed request DTOs |
Usually means gflow-cli was downgraded after a newer version enqueued the task, or the payload was hand-edited; re-enqueue with a compatible version |
31 |
FlowAppError |
Flow did not serve the page gflow asked for. Two shapes: its error-boundary page rendered instead of the editor (a transient client-side crash), or it redirected to flow.google.com/about instead of the project (#756) |
Crash: retry shortly; if it persists, Flow itself is degraded. /about: run gflow auth login --profile <name> and finish Google's "Confirm it's you" if it asks; then open the project in a browser on that host and confirm this account can reach it. A retry is measured not to help — 5/5 consecutive attempts during a live occurrence landed on /about again (2026-09-11; the 2026-09-10 run got 0/5 only because the redirect had stopped reproducing) — so gflow does not flag it retryable. One cause is measured since: Google wanted the account to "Confirm it's you" while its cookies were healthy (2026-09-23); gflow auth login waits on that check (#902) |
32 |
ReferenceNotFoundError |
A referenced media NAME is not in this project's picker. Flow indexes a short auto-caption, not the generation prompt, so a prompt used as a reference name never matches. On flow.google.com it is also raised for an image character (#723) when no picker option is titled exactly the name, several are, or the match is media; after up to ~90 s of reloads, for a catalogued same---project UUID ref (#995) whose image is no longer in the project grid (deleted in Flow); and when an @ reference attaches out of order, landing before a chip already in the prompt (#997), before anything is submitted |
Reference the asset by its media UUID (if Flow deleted it, regenerate it or pass its local file), pass a local file with --ref, or check what exists with gflow data list images. For a character, rename the colliding asset in Flow, or give the character a unique one-word name. Out of order: re-run; if it repeats, report it with the reference count and window size |
33 |
— (gflow doctor verdict) |
Doctor found warn/fail findings — a successful diagnosis, not an error class | Review the report; see gflow doctor |
34 |
SyncPartialError |
gflow data sync failed on some projects but succeeded on others — completed writes stay committed |
Retryable: re-run the same command; it resumes with what is still nameless (see gflow data sync) |
35 |
ExtendUnavailableError |
No Veo extend model is orderable for this account and aspect — the extend family is tier-gated and there is no square variant. Never auto-retry: a tier gate does not clear on its own. | |
36 |
FlowHostMigratedError |
Flow served the project from flow.google.com and the request could not be represented by the migrated composer, or GFLOW_CLI_FLOW_HOST=labs.google disabled it. Supported today: video t2v; local-file video i2v/r2v; image t2i; local-file image i2i; and gflow run --config rows referencing an earlier row (batch:N) or a local file; image i2i --ref <uuid> of an image the catalog records in the same --project (#995); image upscale; video upscale (served here only); and single-segment video extend. Images may tag a character by name (@Name, or --reference-entity with --reference-entity-name, #723); other image UUID references, an entity id without a name, instructions, Imagen 4 and image batch remain unsupported. On an agent-only composer (#799), generation is limited to image t2i and video t2v (one 9:16 or 16:9 clip). Not selector drift (23) |
Not retryable. Use one of the supported forms, or the REST surface (gflow project list, gflow data …); follow #639 for the remaining matrix |
37 |
InsufficientCreditsError |
The account's balance is short for the model it asked for, so Flow replaced the submit control with its Insufficient credits warning instead of disabling it. Short, not necessarily empty: measured 2026-09-07, an account holding 50 credits requesting --model veo-quality (100) rendered the warning. Recognised by the warning's class, so it does not depend on the UI language (#745). Explicitly not selector drift (23): reporting it as drift told users to file a frontend bug over a credit shortfall |
Check the balance with gflow credits user, then pick a cheaper --model (veo-lite costs 10), top up, or wait for the allowance to reset. Nothing was submitted, so no credit was spent. gflow image draws on a separate daily quota and may still work |
38 |
FlowAccountChooserError |
The post-migration hop landed on Google's account chooser and the profile's recorded account (.gflow_account) could not be selected automatically (row absent, click-through did not return to the editor, or --account mismatch) |
Not retryable: run gflow auth login --profile <name> and complete the chooser manually, while signed in as the recorded account (re-run gflow auth login if the chooser offers a different session) |
39 |
FlowAccessUnavailableError |
Flow loaded and routed to its own "you don't have access" screen (<flow-pinhole-unavailable-screen>): this Google account has no Flow entitlement. Detected by component, not by URL — the hop is client-side (flow.google.com/ answers 200) and the path varies (/unavailable, /u/8/unavailable). Explicitly not auth expiry (3/8) and not selector drift (23): nothing expired and nothing drifted |
Not retryable, and signing in again cannot change it. Flow needs an age-verified account in a supported region on a Google AI Plus/Pro/Ultra or qualifying Workspace plan — check which applies at Google's eligibility page and open https://flow.google.com in a browser on this account to confirm |
130 |
SIGINT | User-interrupted (Ctrl-C) | — |
Exit code 16 — data store / migration error. Fires when:
- The database file cannot be opened (filesystem permission or path issues).
- A write could not take the database lock within 5 s (
database is locked— another gflow process was writing). After a successfulgflow imageorgflow videogeneration this is only a warning (data.persistence_failed_after_success, naming the media id): the file is saved and the run succeeds, it is just not recorded in the catalog (#900).gflow movielogs its ownmovie.persistence_failed_*warning; a task on the MCP worker queue records it as the task's error. - A migration fails or the migration checksum drifts from what the installed version expects.
- The database has a newer schema than the installed gflow-cli (i.e. you downgraded after a migration already ran).
Recovery for the "newer schema" case: upgrade gflow-cli to a version that understands the schema (gflow update), OR point GFLOW_CLI_DB_PATH to a different database location (a fresh path creates a new empty database automatically).
All errors emit a structured error_raised event (or error_unhandled for
exit code 1) with stable fields — error_class, problem (RFC 9457 Problem
Details), cli_command, correlation_id. Pipe stderr to a file and grep
for telemetry forensics:
GFLOW_CLI_LOG_FORMAT=json gflow video t2v "..." 2> events.jsonl
jq 'select(.event == "error_raised") | .error_class' events.jsonl
Branch in shell scripts — capture the exit code before the if/case consumes it:
gflow video i2v ./initial.png "test" --out-dir out
rc=$?
if [ "$rc" -ne 0 ]; then
case "$rc" in
2) echo "Bad CLI usage (missing arg, bad flag)"; exit 1 ;;
3) echo "Auth rejected — read the error's remediation_hint; it is NOT always a re-login"; exit 1 ;;
4|6) echo "Transient infra issue (rate limit / network) — try again later"; exit 1 ;;
5) echo "Content policy rejected the prompt — rewrite and retry"; exit 1 ;;
7) echo "Flow API shape changed — upgrade gflow-cli or file a bug"; exit 1 ;;
8) echo "Auth profile is missing a required credential — run: gflow auth login"; exit 1 ;;
9|12) echo "Operation timed out — if it was after submit, check the project in Flow before re-running"; exit 1 ;;
10) echo "Flow rejected the request — adjust the prompt/request and retry"; exit 1 ;;
11) echo "Configuration error — fix the option or env var shown above"; exit 1 ;;
13) echo "Security guard blocked unsafe local state — follow the error guidance"; exit 1 ;;
14) echo "Sign-in rejected the browser (navigator.webdriver) — run: gflow auth login"; exit 1 ;;
16) echo "Database error — check permissions or upgrade gflow-cli"; exit 1 ;;
130) echo "Cancelled with Ctrl-C"; exit 130 ;;
*) echo "Unknown failure (exit $rc)"; exit 1 ;;
esac
fi
Why
rc=$?first? Insideif ! cmd; then ...,$?reflects the negation pipeline (always0when thethenbranch fires), not the failing command. Capturing intorcimmediately after the call is the portable pattern across bash/zsh/dash. PowerShell uses$LASTEXITCODEfor the same purpose.
Programmatic use¶
The CLI is a thin shell over gflow_cli.api.client.FlowApiClient. All public methods used by the commands above are also available directly.
Importing errors¶
Two module paths resolve to the same error classes. Use whichever feels natural for your codebase:
from gflow_cli.errors import GFlowError, AuthExpiredError # canonical
from gflow_cli.exceptions import GFlowError, AuthExpiredError # standard alias
Both are identical objects — gflow_cli.exceptions is a re-export of gflow_cli.errors. The alias exists because many developers and tools expect the conventional exceptions name.
Single-shot generation¶
import asyncio
from pathlib import Path
from gflow_cli.api.client import FlowApiClient
from gflow_cli.api.image import GenerateImageRequest, Model, Aspect
from gflow_cli.config import get_settings
async def main() -> None:
settings = get_settings()
profile_dir = settings.profile_subdir("default")
async with FlowApiClient(profile_dir=profile_dir, headless=settings.headless) as client:
req = GenerateImageRequest(prompt="a peaceful lake at dawn", model=Model.IMAGE4)
# project_id is optional — omit it and a new project is created automatically.
image = await client.generate_image(req=req)
saved = await client.download_image(image, Path("lake.png"))
print(saved)
asyncio.run(main())
project_id defaults to None. When omitted, generate_image() (and generate_images_batch()) call create_project() internally. Pass an explicit project_id when you want multiple generations to land in the same Flow project.
download_image() returns the final write location. That is a local
pathlib.Path by default, or a cloud-backed path when
GFLOW_CLI_STORAGE_URI is set; see EXTERNAL_STORAGE.md.
Archive / cleanup¶
async with FlowApiClient(profile_dir=profile_dir) as client:
project = await client.create_project(title="archive demo")
asset = await client.upload_image(project.project_id, Path("hero.png"))
# Each uploaded asset and each generated media item has its own workflow_id.
await client.archive_workflow(
workflow_id=asset.workflow_id,
project_id=project.project_id,
)
FlowApiClient.archive_workflow(workflow_id, project_id) issues PATCH /v1/flowWorkflows/{id} to soft-delete a workflow. Useful in batch scripts that spin up a project per call and want to clean up afterwards. workflow_id comes from any AssetInfo (upload_image return) or VideoOperation / VideoStatus / ImageResult (generation returns) — ProjectInfo itself only carries project_id and title.
Health check (long-lived workers)¶
For worker processes that hold a FlowApiClient open across many requests, call health_check() before dispatching to detect a dead browser context without catching exceptions yourself:
async with FlowApiClient(profile_dir=profile_dir) as client:
while True:
job = await queue.get()
if not await client.health_check():
# browser context is dead — re-enter or restart the worker
break
image = await client.generate_image(req=job.req)
await handle_result(image)
health_check() returns True if the underlying Playwright page is alive and the current URL is on a Google domain. It returns False (never raises) on TargetClosedError or any other exception.
See also¶
- CONFIGURATION — env vars, output paths, defaults
- AUTHENTICATION — auth flow + multi-account
- ARCHITECTURE — internal structure (for contributors)
- PLAN — what ships in which phase