| name | ff-cli |
| description | Work with FlutterFlow AI CLI workspaces and existing FlutterFlow projects. Use when you need to inspect FlutterFlow pages/components, edit dsl/edit.dart, use generated_code as read-only runtime truth, wire native FlutterFlow actions/API calls/Supabase queries, validate and push with flutterflow ai, or avoid non-editable generated Dart/custom-widget changes. |
FlutterFlow AI CLI
Version: 0.3.1
Updated: 2026-07-06
Change log:
- 0.3.1 - Clarified that the client-side
_ensureUnique guard on app.apiGroup only rejects duplicate declarations within one DSL build, not existing server-side groups. Sourced from thread mra39nlh, where this was the last remaining re-derivation.
- 0.3.0 - Added API Group / Endpoint DSL facts (ensure semantics, header matching, body quoting, escapeStringForJson difference), validate CLI argument requirements, and post-push re-run expectations. Sourced from an efficiency audit of thread mr9x1wih.
- 0.2.3 - Added a battle-tested pointer for custom-code artifact types (class/enum vs widget storage; widgets are not a UI-only edit).
- 0.2.2 - Added a battle-tested pointer for version-history diffing (refresh-context loaded-version scope; non-destructive restore).
- 0.2.1 - Added a battle-tested pointer for action-chain and grouped local-state update wiring.
- 0.2.0 - Added repeatable sync guidance for keeping default
dsl/edit.dart limited to custom-code sync, sectioning it by type, and asking before retaining one-off UI/page/component migrations after a successful push.
- 0.1.0 - Initial public release with core workflow rules and pointers to battle-tested lessons.
Core Rules
- Treat
generated_code/ as read-only runtime truth. Inspect it to understand actual Flutter output, but change FlutterFlow through dsl/edit.dart or FlutterFlow UI steps.
- For existing projects, edit
dsl/edit.dart, not dsl/create.dart.
- Prefer FlutterFlow-native resources over custom Dart: API Calls, Supabase queries, action blocks, built-in formatters, app/page state, component params, and existing functions/actions.
- Do not create custom actions/functions/widgets when a FlutterFlow-native API/action/binding can do the job and remain editable in the UI.
- When custom code is unavoidable, keep it narrow and non-visual unless the user explicitly approves a custom widget.
- Do not patch schema dumps. Use dumps only as reference and provide standalone SQL files for proposed DB changes.
- For existing Supabase projects, read
references/battle-tested-lessons.md before using app.supabase, app.table, Postgres* actions, or action-block wrappers for Supabase mutations.
- For action-chain cleanup, many-field page/component state assignments, or raw local-state update mutations, read the Action Wiring section in
references/battle-tested-lessons.md.
Edit Workflow
- Read
PROJECT_CONTEXT.md, context/api_calls.md, and relevant generated Dart for the page/component.
- Inspect before editing:
flutterflow ai inspect <project-id> --page <PageName> --dsl-json
Use --component for components. If the user provides a selector path, inspect that selector first.
- Make changes in
dsl/edit.dart.
- Validate locally:
dart test
flutterflow ai validate dsl/edit.dart --project-id <project-id>
validate executes the DSL file's main, forwarding CLI args. The starter
template's _parseCliOptions hard-requires --api-key, --project-name,
and --project-id, so either pass all three (use $FF_API_KEY for the key)
or make those args optional in the file with an FF_API_KEY env fallback.
Do not spend turns reconciling this — it is a known template/CLI mismatch.
validate is a local dry run only: it proves the DSL compiles, not that
server-side ensure* comparisons will pass. When unsure whether an ensure
comparison will match, run validate/push and read the error instead of
reading SDK source.
- Push only after validation:
flutterflow ai run dsl/edit.dart --project-id <project-id> --commit-message "<short change summary>"
flutterflow ai refresh-context <project-id>
- After refresh, inspect
generated_code/ to confirm FlutterFlow generated what was intended.
Repeatable Sync And One-Off Migrations
- Keep the default
ai-workspace/dsl/edit.dart limited to safe repeatable sync work, especially repo-owned custom code, custom action metadata, custom functions, and Code Files/classes.
- Section the default
dsl/edit.dart by sync type so stale mutations are easy to spot. Prefer a shape like syncCustomFunctions(project), syncCodeFiles(project), syncCustomActions(project), syncCustomActionMetadata(project), then shared helpers.
- After a successful UI/page/component migration push, ask the user whether to clear or move those one-off mutations out of the default
dsl/edit.dart. Explain that if they are left in place, a later unrelated push can replay them and overwrite live FlutterFlow server changes, FlutterFlow UI edits, or another thread's page/component updates.
- Do not leave page/component/layout/action-chain migrations in the default
dsl/edit.dart after they have been pushed successfully unless the user explicitly wants that migration to remain repeatable.
- Put one-off FlutterFlow UI/page/component migrations in separate dated files under
ai-workspace/dsl/migrations/, and run them explicitly only for that task.
- Before any unrelated custom-code push, check
dsl/edit.dart for stale editPage, editComponent, raw findPage(...)/findComponent(...) mutation blocks, page state migrations, layout changes, action-chain rewiring, or old node-key repairs. Remove, move, or gate them unless the current task is intentionally changing that UI.
flutterflow ai refresh-context updates context/generated snapshots, but it does not update or clean dsl/edit.dart.
- When another thread or the FlutterFlow UI changes a page/component, do not assume refreshing this workspace makes
dsl/edit.dart safe. Inspect and prune stale page/component mutations before pushing.
API Calls
Before adding custom RPC/HTTP code, inspect FlutterFlow API Calls:
sed -n '1,220p' context/api_calls.md
rg -n "GroupName|CallName|endpoint_name" generated_code/lib/backend/api_requests/api_calls.dart
Use FlutterFlow's API Calls library as the editable integration surface:
- If the needed REST/RPC/Edge call already exists, wire that API Call in FlutterFlow actions.
- If the needed call does not exist, create it as a FlutterFlow API Call instead of writing custom HTTP/RPC Dart.
- If an API group already exists with the same base URL, add the new call to that group.
- Create a new API group only when no existing group has the correct base URL or auth/header pattern.
- Keep headers, auth token usage, request body, response parsing, and test values editable in FlutterFlow's API Calls UI.
API Group / Endpoint DSL Facts
Verified against the SDK source; do not re-derive these by reading
ensure_helpers.dart or the DSL compiler.
app.apiGroup(...) compiles to ensureApiGroup (group) plus
ensureEndpointInGroup (each endpoint). Both are idempotent: they match by
name, create when missing, and throw when an existing entity's payload
differs. They never delete or modify existing endpoints, so declaring only
the new endpoints for an existing group is safe and preferred.
- Declaring an already-existing server-side group with
app.apiGroup('Name', ...) is correct, not a conflict. The _ensureUnique(_apiGroupNames, ...)
guard inside the App builder only rejects declaring the same group name
twice within one DSL build — it does not check the server and does not know
about groups created outside the DSL. Declare the existing group once (same
baseUrl and headers) with just the new endpoints; the server-side
ensureApiGroup matches the existing group and ensureEndpointInGroup adds
the new endpoints. Do not reach for app.raw() or updateApiGroup to work
around the guard — you only hit it if you declare the same group name twice
in the same file.
ensureApiGroup compares shared headers with exact, order-sensitive string
equality in Key: Value form (space after colon). Copy header values
verbatim from the inspect output, in the same order. Passing null or []
for headers does NOT skip the check when the existing group has headers — it
fails. Passing baseUrl: null does skip the base-URL check.
- In group headers, use FlutterFlow's native
[variableName] syntax for
shared-variable substitution (e.g. 'Authorization': 'Bearer [jwtToken]').
The DSL's <variable> angle-bracket syntax is for endpoint body: maps and
is not translated in headers; using it there breaks the header match.
- Group shared variables (e.g.
jwtToken) are not compared or re-declared by
ensureApiGroup; an existing group keeps its variables.
- Endpoint
body: is a map whose '<var>' placeholders are JSON-encoded and
then unquoted for non-string types declared in variables:. Strings render
quoted, int_/double_ render unquoted — this matches Supabase RPC JSON
bodies. Supabase type mapping: uuid/text/jsonb → string,
smallint/integer → int_, numeric → double_.
- DSL-created endpoints interpolate string variables directly (
"${arg}")
in generated code, without the escapeStringForJson wrapper that UI-created
calls get. This difference is expected and cosmetic for normal inputs. Do
not rewrite a pushed endpoint (e.g. via bodyText:) to chase it; mention it
in the summary instead.
- After a successful push, re-running the same ensure DSL throws
"already exists" / payload-mismatch errors. This is expected, not a failure:
the work is done. Move the one-off block to a dated file under
ai-workspace/dsl/migrations/ and restore the minimal dsl/edit.dart.
Visual And Runtime Bugs
App Raw Caution
- Use
app.raw() only when the typed DSL cannot express the change.
- Verify raw changes with
flutterflow ai refresh-context and generated code inspection because some raw mutations can compile but not persist as expected.
- For custom action calls in raw proto wiring, prefer the existing project's generated/action shape. Older custom actions use
FFAction.customAction with FFCustomActionCall; newer inline custom code uses customCodeCall.
More Details
Read references/battle-tested-lessons.md when working with existing Supabase projects, action wiring, many-field state updates, custom code upserts, custom-code artifact types (class vs widget storage), raw proto action wiring, component limitations, schema corruption recovery, version-history diffing, or other fragile FlutterFlow AI CLI behavior.