Add an AI agent to a Flutter app, one small step at a time.
The app writes a funny, rhyming "literary skull" (a calaverita) about a friend or coworker from a name, a job and a tone, and draws a sugar-skull portrait to go with it. You start with a plain Flutter app and end with an agent that fills the form, dresses the skull and shows the finished calaverita in the chat, using the CopilotKit Flutter SDK.
Built with Flutter on the web, Very Good Engineering and Feature-First Clean Architecture.
Each folder is a complete, runnable snapshot. Every step starts from the one before it, so compare two folders to see exactly what changed.
| Folder | What the app does | What you learn |
|---|---|---|
00-foundations |
The app with local state and no agent. You fill the form, dress the skull and press a button. | The architecture: a domain, a data and a presentation package per feature. |
01-shared-state |
A copilot panel appears. You tell it about your friend and it fills the form and dresses the skull. | Shared state: AgentStateBuilder, patchSharedState and setSharedState. |
02-generative-ui |
You ask for the calaverita and the agent writes it: a card with the skull appears in the chat and the verse shows under the skull on the stage. | Generative UI: a frontend tool registered with CopilotTool whose builder renders a widget. |
03-human-in-the-loop |
You ask for a calaverita without a tone and the agent asks you how to roast, with buttons in the chat. | Human in the loop: a tool registered with CopilotHumanInTheLoop, whose run waits for your answer. |
Install FVM and Node.js 20 or later, then run one command:
node tool/setup.mjsIt installs Flutter 3.47.0 through FVM, checks your machine, asks which step to run and which backend to use, and starts the app in Chrome. The backend is one of:
- The shared backend: the one we host for the workshop, at
https://calaverita-runtime-641689536796.us-central1.run.app/api/copilotkit. Nothing to set up except the workshop password, which you get at the workshop. It asks for it (hidden as you type), or pass--code <password>. - Your own runtime: it asks for your Anthropic API key (hidden as you type)
and starts the runtime in
runtime/for you. - A custom backend: any CopilotKit runtime you have a URL for, for example one
made with CopilotKit's own tool (
npx copilotkit with-claude).
node tool/setup.mjs --help lists the options, such as --step 02 --backend local
to skip the questions. The rest of this page does the same thing by hand.
You need:
- FVM. The repository pins Flutter 3.47.0 in
.fvmrc, and the SDK needs Dart 3.13 or later. - Node.js 20 or later, for the runtime.
- An Anthropic API key, only to run your own runtime. The shared backend needs none.
- Chrome.
fvm installThen check that everything is in place. It runs the same on Windows, macOS and Linux, and it never prints your key:
node tool/doctor.mjsIt checks your machine and the shared backend. Your own runtime is optional, so
what it still lacks is listed as information (·), not as a warning.
Note
Windows: turn on Developer Mode (Settings → System → For developers)
before fvm install, because FVM uses symlinks. The commands below work in
PowerShell and in a macOS or Linux terminal. Where they differ, both are shown.
Note
The CopilotKit Flutter SDK is not on pub.dev yet, so each snapshot gets it from
GitHub, pinned to one commit (see dependency_overrides in the pubspec.yaml
at the root of each snapshot). Your first pub get needs to reach
VGVentures/copilotkit_flutter_sdk.
The app talks to a Copilot Runtime, a small server that sits between the app
and the model. Steps 01 to 03 use the one in runtime/.
1. Start the runtime (steps 01 and 02) in a first terminal:
cd runtime
npm installCreate your runtime/.env from the example, then open it and set
ANTHROPIC_API_KEY=<your key>:
cp .env.example .env # macOS and Linux
Copy-Item .env.example .env # Windows PowerShellnpm startIt prints http://localhost:8200/api/copilotkit. Check it from another terminal
with node tool/doctor.mjs.
Important
The key lives only in runtime/.env (git-ignored) or in your shell. Never
commit it.
To use another model, set COPILOTKIT_MODEL in runtime/.env. The default is
anthropic:claude-sonnet-5-5.
2. Run the app in a second terminal. 00-foundations needs no runtime.
cd 01-shared-state # or 00-foundations, 02-generative-ui, 03-human-in-the-loop
fvm flutter pub get
cd apps/calaverita_app
fvm flutter run -d chromeEach step adds one thing the one before it couldn't do. The Write my calaverita button always uses the offline template, so it works in every step. From step 02 the agent can write a verse too.
To see what a step adds, compare its folder with the one before, from the root of
the repository (add --stat for the list of files):
git diff HEAD:00-foundations HEAD:01-shared-state -- ':!pubspec.lock' ':!**/test/**'Steps 01 to 03 end with a challenge for Claude Code, about five minutes each. They change only the app, so they work with the shared backend too.
- Type a name and a job. The button stays disabled until both are filled.
- Pick a tone, then put a hat or glasses on the skull and change its color.
- Press Write my calaverita.
You should see: the skull change as you click, and a four-line verse under it. There is no chat and no agent yet.
Type in the Copilot panel: "Mario is a chef, dramatic please". You can also click a suggestion.
You should see: the name, the job and the tone fill in, the skull gets dressed, and the Shared state box shows the same values. The agent replies in a sentence and tells you to press Write my calaverita.
Then edit a field by hand and ask "who is this for?". The agent answers with your edit, because it reads the same shared state.
Not yet: the agent can't write the verse. Press Write my calaverita to get the template one.
Challenge: add a nickname to the form. It goes in the shared state with the name and the job, so the agent sees it and fills it in when you say "his nickname is El Sazón". Ask Claude Code: "Add an optional nickname field to the calaverita draft and the form, in 01-shared-state."
Type: "Mario is a chef, dramatic please. Write his calaverita!"
You should see: the form fill in and the skull get dressed, then a card with the skull portrait and a four-line verse appear in the chat. The same verse shows under the skull on the stage.
If the name or the job is missing, the agent asks for it instead.
Challenge: give the calaverita a title. Add a title argument to the
showCalaverita tool and show it on the card: the agent reads the tool's
parameters, so it writes one with no change to its prompt. Ask Claude Code:
"Add a short title argument to the showCalaverita tool in 02-generative-ui and
show it at the top of the card."
Type a request without a tone: "Write a calaverita for Mario, a chef". You can also click that suggestion.
You should see: the form fill in with Mario and chef, then a Question card in the chat: "How should I roast Mario?" with Playful, Sarcastic and Dramatic. The agent waits. Tap one, and it sets the tone, writes the calaverita and shows it in the chat and on the stage.
Name a tone in your message ("Write a sarcastic calaverita for Mario, a chef") and it doesn't ask. While the question waits, the copilot isn't "working": the Stop button and the typing dots go away, and the message box points to the card.
Challenge: let the agent ask with colors instead of words. Add a
pickColor tool like askUser, whose card shows color swatches, then say
"let me pick the skull color". The tool's description is enough for the agent
to use it. Ask Claude Code: "Add a pickColor human-in-the-loop tool in
03-human-in-the-loop, like askUser, whose card shows four color swatches and
returns the hex color, then set the skull color with it."
The runtime URL is set in one place per snapshot:
apps/calaverita_app/lib/app/app_config.dart. To use a hosted backend, change
AppConfig.defaultRuntimeUrl, or run with
--dart-define=RUNTIME_URL=<url>.
The shared backend (https://calaverita-runtime-641689536796.us-central1.run.app/api/copilotkit)
also asks for a password. The app sends it, and a random id
of the run, as the X-Workshop-Code and X-Workshop-Session headers on every
request:
fvm flutter run -d chrome --dart-define=RUNTIME_URL=<url> --dart-define=WORKSHOP_CODE=<password>--dart-define compiles the password into the build, so a build made with
WORKSHOP_CODE contains the password: anyone who has that web build can read
it. Run it locally, and never host a build made with the code.
A runtime on your machine ignores both headers, so leave WORKSHOP_CODE out
there. To host the shared backend, deploy runtime/ and set WORKSHOP_CODE and
ANTHROPIC_API_KEY on the host. The runtime then answers 401 without the
password. GET /info and the CORS preflight stay open.
The runtime also logs one line per run with the session id, and answers 429
once a session passes MAX_RUNS_PER_SESSION runs (default 100) or all sessions
pass MAX_RUNS_TOTAL (default 5000). Change WORKSHOP_CODE after the event,
and set a spend limit on the Anthropic key: --max-instances=1 limits
concurrency, not cost.
A hosted backend that has been idle takes a few seconds to wake up, so the
first check or message can be slow. To warm it before the event, run the
warm_runtime workflow from the Actions tab.
Each step is a Dart workspace of small packages:
apps/calaverita_app/ composes the features into an app
features/calaverita/
calaverita_domain/ models and the repository interface
calaverita_data/ the offline template that writes the verse
calaverita_presentation/ the screen, the cubit and the skull portrait
features/assistant/
assistant_presentation/ the chat (from step 01)
shared/app_ui/ the design system
A presentation package never imports a data package, and a feature never reaches
into another feature's internals. Run fvm flutter analyze and
fvm flutter test inside a snapshot, and fvm dart test in the domain and data
packages.
- Give the skull more parts: a beard, a different pattern, a bandana. Add the SVG
and the variant in
SkullPart, and teach the agent in the prompt. - Let the agent ask more: "make it fancier" could offer three skull styles with
the
askUsertool of step 03. - Replace the template repository with a call to the agent, so the Write my calaverita button uses the model too.
- Save the calaverita as an image, or share it.
- Run your own agent server.
extras/custom-server/serveris a small custom one that checks every change the agent makes to the form. Start it (npm install, thennpm start) and point the app at it withnode tool/setup.mjs --backend custom --url http://localhost:4100/api/copilotkit --agent calaverita.
- "The copilot could not answer": the app can't reach the runtime. Check that
it is running and that the URL in
app_config.dartmatches the one it printed. - The runtime starts but every message fails: the model or the key is wrong.
Send a run straight to the runtime and read the
RUN_ERRORevent (see the SDK example README). - "The copilot could not answer" on the shared backend: the password is
missing or wrong (
401). pub getcan't fetch the SDK: you need access to the SDK repository.
MIT. The skull art and the design system come from
calaverita_builder, the fonts
in shared/app_ui keep their own licenses, and the CopilotKit and Very Good
Ventures names and logos belong to their owners.
Leading a team that wants to ship AI features in Flutter? Talk to Very Good Ventures.