Upgrading projects¶
A new CLI release brings new templates. scaffold upgrade merges
them into your project with a 3-way merge against the templates of the exact build that
created it, so your edits are kept and only the files you never touched are replaced.
Upgrade or enhance?¶
| You want to | Use | What it does |
|---|---|---|
| Move the project to a newer CLI version | scaffold upgrade |
Merges the template changes between the build that created the project and the running build. |
| Change a setting: CD mode, runtime, model provider, registry, deployment target | scaffold enhance |
Re-renders the scaffolding for the new settings, at the project's own version. |
Upgrade a project¶
-
Update the CLI (and the skills with it):
-
Preview the upgrade. Nothing is written:
-
Apply it.
-yapplies every non-conflicting change;-ialso walks you through each conflict. A backup is written first. -
Check the result:
-
Read the release's migration notes in the changelog before you deploy, and follow Upgrade a running deployment.
A project that is up to date says so:
$ graph-agents-cli scaffold upgrade --dry-run
✅ Project is already at version 0.2.0 (build 0.2.0+g181aebc)
How the merge works¶
scaffold upgrade re-renders the project's settings twice: with the build that created it (the
baseline, run through uvx) and with the running build. It compares both with your files:
| Files | What upgrade does |
|---|---|
Agent code: app/agent.py, app/tools/**, app/policies/**, app/prompts/**, app/graph/** |
Never modified. |
Configuration: .env, .env.*, api-policy.yaml, deployment/, deployment/, tests/, tests/ |
Never overwritten. |
Dependencies: pyproject.toml, the manifest |
Merged semantically. |
Scaffolding: everything else (values.yaml, chart templates, app/app_utils/**, app/, the Dockerfile, the workflows) |
Replaced when you did not edit it; a conflict when both you and the template changed it. |
| Files you added, in neither template | Left alone. |
Conflicts are listed, or resolved one by one with -i. The backup goes to
~/.graph-agents-cli/backups/ (private: 0700, .env* files 0600), which keeps the newest five
backups of each project.
Check values.yaml after an upgrade
A chart values.yaml that you and the template both changed is kept whole, so settings the
new template adds (the shutdown drain, for example) are silently missing
(KI-040).
Compare it with a fresh create of the same settings, merge by hand and check the result
with helm template. Your values-<env>.yaml files are never touched. Lines the CLI's own
commands wrote count as your changes: the base URL api add writes and, from 0.3, the
settings system apply writes, so a project wired by system apply has this conflict at
every upgrade.
Which build created the project¶
The manifest names the baseline:
cli_version: '0.2.0'
# The build that rendered this project and a digest of it; `scaffold upgrade` reads both
cli_build:
id: 0.2.0+g181aebc
commit: 181aebc93fcc537ebb87420128f354eb9a3f3dd0
template_digest: sha256:e2d79261cfe8a6dde6482fee880c787e7582ca89a48e2c19b2b4c839372a7c50
generated_at: '2026-09-25T02:51:13.329087+00:00'
cli_versionis the release.cli_buildis written bycreate,enhanceandupgrade: the build id thatgraph-agents-cli --versionprints (0.2.0for the release,0.2.0+g<commit>for a build between releases,.dirtyadded for uncommitted changes), its full commit, andtemplate_digest, a digest of what that build renders for the project's settings.generated_atis when the project was created.
graph-agents-cli info shows it as Scaffolded with: 0.2.0 (build ...). From these fields:
| The manifest records | The baseline is |
|---|---|
| an older release | the release tag v<cli_, or the recorded commit when cli_build names a build between releases |
the running version, the same build or the same template_digest |
none: "already at version" |
| the running version, another build | the recorded commit |
no cli_build (a project made before builds were recorded, for example by a pre-release 0.2.0 build) |
compared by version only: "already at version", with how to name the build |
After an upgrade the manifest records the running build, so the next upgrade needs no flag.
Every release is tagged v<version> on the repository (v0.1.0, v0.2.0, v0.3.0, v0.3.1), and that tag is
the default baseline for the projects the release created.
Name the baseline with --baseline-ref¶
--baseline-ref REF names the build that created the project and wins over the manifest:
REF |
Example |
|---|---|
| a commit or tag of the repository | 1a2b3c4, v0.1.0 |
<clone>@<commit>: a commit in a local clone, looked up there first (the way to reach a commit that was never pushed) |
~/src/ |
| a path to a checkout or a wheel, rebuilt, never a stale uv cache | ~/src/ |
| a full install spec, for a mirror | git+https://git. |
The baseline must render the manifest's cli_version; a build of another version is exit 3.
Without a known commit, the newest one before the project was generated is a first candidate:
git -C <clone> log -1 --format=%H --before='<generated_at from the manifest>'
graph-agents-cli scaffold upgrade --baseline-ref <clone>@<commit> --dry-run
graph-agents-cli scaffold upgrade --baseline-ref <clone>@<commit> -y
The build may be older than that commit (a checkout behind its branch, or a stale uv build), so
always read the dry run first. With the right build, only files you edited are listed under
"Will preserve" or as conflicts. Many scaffolding files you never touched there mean a later
build than the one that created the project; upgrade warns when most template files would
keep their current content. Applied with -y anyway, a wrong baseline records the project as
up to date while its files stay old
(KI-041);
run again with the right --baseline-ref to recover.
A build between releases has no tag: upgrade rebuilds it from the commit cli_build records,
fetched from the repository. A commit that was never pushed is named with
--baseline-ref <clone>@<commit>. A build with uncommitted changes (.dirty) cannot be
rebuilt at all: name the closest commit with --baseline-ref.
--baseline current¶
--baseline current compares against the current templates instead of the build that created
the project. It is an explicit, logged opt-in, and the summary is labelled as such. It cannot
tell your edits from the template changes since the old version: every file listed under "Will
preserve" that you did not edit keeps its old content, and dependency changes are not merged.
Use it only knowingly, and never for a 0.1.0 project.
Install the baseline from a mirror¶
setup, update, the upgrade baseline and generated projects' CI install the CLI from a pinned
git tag of the repository. GRAPH_AGENTS_CLI_INSTALL_SPEC points them at a private mirror, a
wheel or a package index. Write {version} where the version goes, so the baseline can install
an older release:
export GRAPH_AGENTS_CLI_INSTALL_SPEC='git+https://git.example.com/graph-agents-cli@v{version}'
graph-agents-cli scaffold upgrade --dry-run
- An override without
{version}is refused for the baseline (exit 3): it would install one fixed build in place of the old version. {version}is a release number (thecli_versiona project records), so the override names releases only. Name a build between releases with--baseline-ref.- An override with control characters or whitespace is refused (exit 3), except the spaces of
a PEP 508
name @ urlreference. - At
createtime the override also becomes the new project'sGRAPH_AGENTS_CLI_SPECin.github/agent.env.
Exit codes¶
| Exit | scaffold upgrade |
|---|---|
| 0 | upgraded, previewed, or already at version |
| 2 | the baseline could not be built: uvx is missing, or could not fetch and run the old build (no tag on the remote, a commit never pushed, no network). Nothing was changed. |
| 3 | not in a project; the manifest's cli_version is missing or not a release; the recorded build cannot be rebuilt (.dirty, or a build between releases while an install-spec override is set); --baseline-ref names no build or a build of another version; an unusable GRAPH_ |
See Exit codes for every command.
Change settings with scaffold enhance¶
scaffold enhance adds or changes the deployment target, CD mode, runtime, model provider or
registry of an existing project, with the same 3-way merge after a backup:
graph-agents-cli scaffold enhance --cd argocd --dry-run
graph-agents-cli scaffold enhance --runtime langgraph-server
graph-agents-cli install # after --runtime: updates uv.lock
- It works at the project's own version: when the manifest names another version, it runs that
version's CLI through
uvx(exit 2 withoutuvx). - A runtime or model-provider change also updates the model default,
secrets.keys,.env.example, the chart values (key by key, around your edits) and.github/agent.env. What it cannot apply is listed under "Left for you"; steps marked(required)make it exit 1, and an edited Dockerfile gets the new version beside it asDockerfile.new. - It never touches
api-policy.yaml: change the policy withgraph-agents-cli api. - Required steps are reported only by the enhance that changes the settings, so act on the first run's "Left for you" list (KI-098).
Both enhance (a settings change) and upgrade (to a new version) rewrite the manifest
without its comments.
Migrations¶
The changelog lists every breaking change with its migration steps. For 0.2.0 there are three:
-
Roll out with
Recreateor at one replica, create the metrics Secret, check the shutdown settings, update clients and eval datasets. -
Migrate
product-policy.yaml, upgrade against thev0.1.0baseline, then port agent code and values files by hand. -
Find the commit that created the project and name it with
--baseline-ref.
0.2 to 0.3¶
0.3 adds agents calling agents: the actor-aware principal, token exchange,
relayed approvals, peer and system; and
structured final answers, an agent that answers in a
JSON shape the project declares. An existing project keeps its behaviour after
scaffold upgrade unless one of the edits below applies to it. The complete list of changes
is in the changelog.
In this order¶
- Update the CLI and the skills:
graph-agents-cli update. The skills carry the new rules for coding agents, including how to declare other agents. - Upgrade the runtime:
graph-agents-cli scaffold upgrade --dry-run, then-y. It three-way merges the template-owned runtime (app/app_utils/,app/fast_api_app.py, the chart templates) and addsapp/app_utils/a2a_client.py,app/app_utils/token_exchange.pyandapp/app_utils/structured.py.app/agent.py,app/tools/**andapp/policies/**are never touched, and none of them needs a change unless the agent is to answer in a JSON shape (below)..env.exampleand thevalues-<env>.yamlfiles are yours and are not rewritten: the new settings are in the environment reference. - Then declare other agents, with
peer addorsystem apply.peer addrefuses a project whose runtime is still 0.2 andsystem checkreports one (SC01): a 0.2 runtime refuses every call onceapi-policy.yamlusesprotocol: a2a. - After every later upgrade or
apiedit, rungraph-agents-cli peer syncin a project with peers:scaffold upgradenever touchesapp/tools/, andlintfails whiletools/a2a_peers.pyand the policy differ. - Deploy as Rolling it out describes.
What needs your edit¶
- An
auth: forwardAPI that can never send a credential (undershared-bearer, underjwtwithoutforward_audience, or under thelanggraph-serverruntime) stops the app from starting outsideAPP_ENV=dev, andlintreports it (exit 3). Every call to such an API already failed with "the caller has no credential". Give itforward_audience, move it toauth: exchange, or remove it (scaffold upgradenever rewritesapi-policy.yaml); underAPP_ENV=devthe app logs why and starts. jwtreads the RFC 8693actclaim. A token carrying one is an agent's for the user, refused with 403 untilAUTH_ALLOWED_ACTORSlists that agent. If your identity provider already putsactin tokens that people use directly, list the agents or setAUTH_JWT_ACTOR_CLAIM=(empty) to read every token as the user's own, as 0.2 did.- Agents other agents call list their callers in
AUTH_ALLOWED_ACTORS(empty refuses every agent), withAUTH_JWT_AUDIENCEnaming the agent.system applysets both. - Custom policies (
app/policies/**is never upgraded): a policy through which another agent forwards users' credentials must setPrincipal.actor, or this agent treats the calling agent as the person. A policy that returns an empty id, one over 256 characters or one with control characters now fails the request with 500 and logs the bug. - Structured final answers, only if you adopt them. A project answers in text until
app/response_schema.jsonexists, and nothing in 0.2 creates that file. Before adding it, wireapp/agent.pyby hand, sincescaffold upgradenever rewrites it: importStructuredAnswerandresponse_formatfromapp_utils.structured, passresponse_format=response_format(model, tools)tocreate_agent, and putStructuredAnswer()last inmiddleware(), as a new project'sagent.pydoes. Then rungraph-agents-cli lint: it refuses a schema the agent would not start with (exit 3) and warns while either piece of wiring is missing. Without the wiring, every run with a schema ends with theerrorcodeinvalid_structured_response: the runtime checks every answer again before it delivers it on/chatand over A2A. An answer that did not go throughStructuredAnswer()still stays in the thread, where the thread's messages and LangGraph Server's native API return it (KI-172), so wire both pieces.scaffold upgradealso brings the tests'tests/conftest.py, which runs them with the mode off (RESPONSE_SCHEMA_PATH=none), andtests/unit/test_structured.py, which checks your schema and thatagent.pyanswers in it. See A project created before 0.3. - Hand-written peer clients (a tool that posts A2A JSON-RPC itself, a delegating auth
policy that exchanges tokens in
authenticate): delete the delegating policy and its registration inapp/policies/__init__.py, remove the APIs it used (graph-agents-cli api remove <peer>_agent, and any separate approvals API), declare each peer withgraph-agents-cli peer add <peer>(orsystem apply), then delete the hand-written tool module.
What changes by itself¶
- Database. At startup, under the schema lock,
ADD COLUMN IF NOT EXISTSaddsthreads.actor,runs.actorand the approvals columnsrequester_actor,decide_with,relayers,decided_viaanddisplay_digest(agent_*tables underlanggraph-server); no row is rewritten, and existing rows read as direct. A pending approval asked before the upgrade isdirectand is decided exactly as before. Underlanggraph devthe approvals file is read as version 1 and written as version 2. - A2A tasks move to Postgres under
CHECKPOINTER=postgres(and a PostgresDATABASE_URIunderlanggraph-server): tablea2a_tasks(agent_a2a_tasks), created at the next start. Every replica sees every task and tasks survive restarts; a task whose run ended with its process turnsfailed; a subscription or cancel that reaches a replica other than the one running the task is refused (-32004,-32002, KI-024).A2A_TASK_TTL_S=0now keeps a task until its thread is deleted (0.2: until the process restarted).CHECKPOINTER=memorykeeps the in-memory store. - A2A tasks follow their approval: an
input-requiredtask ends as its approval does, also when the person decides over HTTP or the approval expires. The approval request part addsapproval_json(the approvals as exact JSON text) and its text shows each call's body; a failed task carries an error data part ({"type": "error", "code": ...}); the agent card declares the origin extension. See HTTP API. - The approval object gains fields (
decide_with,requester_actor,decided_via,digest, and for JSON-RPC callsrpc_methodanda2a_operation), and its times read from Postgres are in UTC whatever the database session's time zone. - Correlation across agents. Calls to other agents (
protocol: a2a) and toauth: forwardandauth: exchangeAPIs carry the request'sX-Request-IDand, under OTLP tracing, its W3C trace context; an incomingtraceparenton the A2A routes continues the caller's trace. Other APIs receive neither, and atraceparenton/chatis not continued.PROPAGATE_TRACE_HEADERS=offturns both off;allsends them to every API and continues a trace on every path. See Observability. - Under
langgraph-server, a native run acts for its caller, whatever run context the request sends: its tools see the caller's own id, roles and actor. A client that set another principal's context on a native run no longer can (Studio underlanggraph devkeeps the context it sends). - Tool output that is not valid Unicode (a lone surrogate) reaches the model, the stream and A2A replies with U+FFFD in its place instead of failing the run.
eval'sjson_schemacheck reads the reply's answer. 0.2 parsed the first fenced block, else everything from the first{or[; 0.3 reads the whole reply when it is JSON, else its last JSON object or array (of the schema's root type), and in a project with a response schema the run'sstructured_responseitself. A case that passed on an example the reply quoted before its answer can now fail, and one that failed on prose after the JSON can now pass. See Evaluation.- Streamed requests to OpenAI-API models ask for token usage (
stream_options), so runs on anopenai-compatibleendpoint record their tokens. - New settings default to 0.2's behaviour:
MODEL_REASONING_EFFORTandMODEL_USE_RESPONSES_APIunset; no response schema (RESPONSE_FORMAT_STRATEGYacts only with one); nolimits.max_response_bytes;protocol: httpfor every API;decide_with: directfor every gate; theA2A_*andAUTH_*delegation settings only act on requests other agents send. - The CLI works under a SOCKS proxy (it depends on
httpx[socks]),run --stop-serverexits 2 when it cannot stop a server,infoskipsnpx skills listunderGRAPH_AGENTS_CLI_NO_UPDATE_CHECK=1or in CI, anddeployneeds no Secret key for a keyless project.
Rolling it out¶
The new columns and the task table are created at startup under the schema lock, so new pods can start beside old ones. Two things need care while both versions run:
- Tasks. Tasks a 0.2 pod holds in its memory are lost when it stops, as at any 0.2 restart; tasks a 0.3 pod creates are in Postgres.
- Wire callers last. A 0.2 pod does not read
act, so it takes another agent's token as the user's own. Add the callers toAUTH_ALLOWED_ACTORS, and declare peers in the calling agents, only once every pod of the called agent runs 0.3.
0.2.0 and 0.3 pods hold the same Postgres run lease, so they exclude each other on a thread. A deployment still running 0.1.0 or a pre-release 0.2.0 build follows Upgrade a running deployment instead.
Downgrading¶
scaffold upgrade has no downgrade guard
(KI-102). Under
0.2 the added columns and the task table are ignored, A2A tasks go back to process memory,
and the threads agents started for a user are reachable by any token for that user, another
agent's included, since 0.2 does not read act.
Upgrade a running deployment¶
Old pods (0.1.0 has no run lock across replicas; pre-release 0.2.0 builds used a session
advisory lock) and new pods (a Postgres lease) do not exclude each other. While both run, one
thread can run on an old and a new pod at once. Upgrade with a Recreate rollout or at one
replica.
The chart has no rollout strategy value
Even at one replica, the default RollingUpdate starts a new pod before the old one stops
(KI-032).
Scale the Deployment to 0 before the upgrade deploy, or patch its strategy to Recreate by
hand.
The new tables are created at startup. With
metrics.serviceMonitor.bearerToken.enabled, run graph-agents-cli secrets apply --env <env>
once after the upgrade so <release>-metrics exists.
A project created with 0.1.0¶
The authentic baseline is 0.1.0 itself, from the v0.1.0 tag (commit fc3f2f9). When the tag
cannot be fetched (an offline mirror, a fork without tags), name the commit in any clone that
holds it:
git clone https://github.com/ss7172/graph-agents-cli /tmp/gac # or your mirror
graph-agents-cli scaffold upgrade --baseline-ref /tmp/gac@fc3f2f9 --dry-run
graph-agents-cli scaffold upgrade --baseline-ref /tmp/gac@fc3f2f9 -y
Then port what upgrade never rewrites (app/policies/__init__.py, app/agent.py,
app/tools/, the values-<env>.yaml files) as the changelog describes.
Never --baseline current for a 0.1.0 project
Nearly every scaffolding file changed in 0.2.0, so the project would keep 0.1.0's
app_utils, chart and workflows, its tests would fail to import and /chat would answer
500. If you ran it, restore the backup it printed, or restore from git, and upgrade with
the authentic baseline.
Limitations¶
| Limitation | What to do |
|---|---|
An edited chart values.yaml is kept whole, dropping new settings (KI-040). |
Compare with a fresh create and merge by hand. |
A wrong --baseline-ref applied with -y records a stale project as up to date (KI-041). |
Always --dry-run first; re-run with the right baseline. |
scaffold upgrade has no downgrade guard: an out-of-date CLI applies its older templates (KI-102). |
Compare graph-agents-cli --version with the manifest's cli_version and cli_build, and preview with --dry-run. |
The manifest loses its comments when enhance or upgrade rewrites it (KI-096). |
Restore them from version control. |
| Remote templates skip symlinks, and upstream scaffold-engine fixes are ported by hand (KI-093). | Use real files in templates. |
Next steps¶
-
cli_version,cli_buildand every other field. -
Breaking changes and migration steps for each release.
-
Roll the upgraded project out.
-
What 0, 1, 2 and 3 mean for every command.