agent deploy
Deploy a CI-built agent image to the initializ AI Platform — the deploy spec, forge.yaml and build-output inputs, the claude-agent runtime, metadata tags and raw annotations, network reachability, stateful storage, HTTP invoke, env var interpolation, and --wait semantics.
initializ agent deploy submits a CI-built agent image to the platform. For
the default forge runtime it composes a deploy request from
initializ-deploy.yaml, forge.yaml, the repo's SKILL.md files, and the
forge build output directory (.forge-output). For
agent.type: claude-agent there is no
forge.yaml — the governed image is built in CI, and the model gateway,
workload identity, policy enforcement, and audit wiring are configured
server-side. Either way, the platform renders the Kubernetes manifests and
rolls the agent — the image must already be pushed to the workspace
Environment's registry by your CI job.
initializ agent deploy -f initializ-deploy.yaml [--image <ref>] [--wait]Key semantics:
- Upsert on (workspace, agent name): the first deploy creates the agent, later deploys update it.
--imageoverrides the spec'simage, so CI-templated tags need noyq/sedrewriting of the manifest.--tag key=valueadds metadata tags stamped onto the agent's pods as annotations (repeatable; overrides the spec'sagent.tags) — handy for injecting$GITHUB_SHAand the like.--annotation key=valuestamps a raw pod annotation verbatim — for keys that must keep their exact name, likeprometheus.io/scrape=true(repeatable; overrides the spec'sagent.annotations).- With
--waitthe command polls until the rollout finishes — exit code 5 on failure, 6 on timeout. This is the CI-friendly mode.
agent deploy flags
| Flag | Shorthand | Default | Description |
|---|---|---|---|
--file | -f | initializ-deploy.yaml | Path to the deploy spec |
--image | Image ref to deploy (overrides the spec's image) | ||
--tag | Metadata tag key=value stamped as a pod annotation (repeatable; overrides the spec's agent.tags) | ||
--annotation | Raw pod annotation key=value stamped verbatim (repeatable; overrides the spec's agent.annotations; reserved prefixes are rejected) | ||
--wait | false | Poll until the rollout finishes | |
--timeout | 0 (→ 10m) | Max time to wait (Go duration, e.g. 15m) | |
--poll-interval | 0 (→ 5s) | Status poll interval | |
--skip-env-check | false | Don't fail on missing required env vars |
All global flags apply. The target workspace is
resolved in this order: --workspace / INITIALIZ_WORKSPACE_ID / config
file, then the spec's agent.workspace, then the workspace baked into the
token. If none resolves, the command fails with exit code 2.
The deploy spec (initializ-deploy.yaml)
The spec lives in the agent's git repo next to forge.yaml. Fully annotated:
apiVersion: initializ.ai/v1
kind: AgentDeploy
agent:
# Display/lookup name on the platform. Deploys are an UPSERT keyed on
# (workspace, name). Defaults to forge.yaml's agent_id when omitted.
name: support-agent
# Agent runtime type. v1 supports "forge" (the default) and "claude-agent".
type: forge
# Workspace id (ws_…). Optional — falls back to --workspace,
# INITIALIZ_WORKSPACE_ID, then the workspace baked into the token.
# workspace: ws_a1b2c3de
# Optional key=value metadata tags, stamped onto the agent's pods as
# annotations. Values stay literal — dynamic values (commit SHA, run id)
# come via --tag, which the shell/CI expands. created-by and workspace-id
# are reserved and added by the platform.
tags:
team: support
# Optional RAW pod annotations, stamped VERBATIM (no namespacing) — for
# keys a controller reads by exact name. Dynamic values come via
# --annotation. Reserved prefixes (kubernetes.io/, k8s.io/, initializ.ai/)
# are rejected by the platform.
# annotations:
# prometheus.io/scrape: "true"
# prometheus.io/port: "9090"
# Image your CI just built & pushed to the registry configured on the
# workspace's Environment. --image overrides this.
image: registry.initializ.ai/acme/support-agent:latest
forge:
# Path to forge.yaml, relative to this file. Default: ./forge.yaml
path: ./forge.yaml
# forge build output — the CLI reads the image's declared env-var union
# from build-manifest.json (or k8s/secrets.yaml). Default: ./.forge-output
outputDir: ./.forge-output
# Environment variables for the agent container.
env:
- name: LOG_LEVEL
value: info
- name: OPENAI_API_KEY
value: ${OPENAI_API_KEY} # interpolated from the CI environment
secret: true # lands only in the agent's Kubernetes Secret
- name: FEATURE_FLAG_X
value: ${FEATURE_FLAG_X}
optional: true # silently omitted when ${FEATURE_FLAG_X} is unset
# Optional container port when the agent doesn't listen on forge's default 8080.
# port: 9000
# Optional runtime sizing; omitted fields use platform defaults.
resources:
replicas: 1
requests: {cpu: 250m, memory: 256Mi}
limits: {cpu: "1", memory: 1Gi}
# Extra egress domains merged additively with forge.yaml's
# egress.allowed_domains and skill-declared egress domains.
egress:
additionalDomains:
- api.stripe.com
# Optionally expose the agent beyond the cluster — see "Network
# reachability (the ingress block)" below. Works on both runtimes.
# ingress:
# reachability: private # cluster | private | public
# Default deploy behavior (flags always win).
deploy:
wait: true
timeout: 10mValidation rules (violations exit with code 2): apiVersion must be
initializ.ai/v1 when set, kind must be AgentDeploy when set,
agent.type must be forge or claude-agent when set, port must be
0–65535, and every env entry needs a non-empty name. A claude-agent
spec additionally requires model.provider (anthropic or openai); the
a2a block is only valid on non-forge runtimes, and a2a.auth must be
empty, bearer, or none (matched case-insensitively).
The newer blocks add their own rules, still exit code 2 locally:
ingress.reachability must be empty, cluster, private, or public
(case-insensitive — whether a mode is actually offered is checked by the
platform); storage and initContainers are only valid on non-forge
runtimes, every storage volume needs name, size, and mountPath,
every initContainers entry needs a name and command, and
initContainers require at least one storage volume (they exist to
populate it); http is only valid on non-forge runtimes, is mutually
exclusive with a2a.enabled, its path must start with /, its method
(when set) must be GET, POST, PUT, PATCH, or DELETE, and setting
http.input or http.method without http.path is an error.
Env value interpolation
env[].value supports ${VAR} interpolation from the CI process
environment only — secret values live in the CI secret store, never in the
file. Interpolation applies exclusively to env[].value; the rest of the
document (image, name, egress) stays literal, so what gets deployed is
auditable from the file plus the --image flag alone.
- An unresolvable
${VAR}reference fails the deploy with exit code 2 — unless the entry is markedoptional: true, in which case it is omitted from the request entirely. $$escapes a literal$; a bare$(not followed by{) is literal.secret: trueroutes the value into the agent's Kubernetes Secret only — the platform never persists it.
The env-var contract check
The CLI knows which env vars the image requires — the union of skill
requirements, channel vars, and model-provider keys computed at
forge build time — and fails fast (exit 2) when a required var is neither in
env: nor provided as a secret. Source-of-truth order:
.forge-output/build-manifest.json— theenv_required/env_optionallists (structured; present in recent forge versions)..forge-output/k8s/secrets.yaml— thestringData:keys; a key preceded by a# optionalcomment line counts as optional (older forge versions).SKILL.mdfrontmatter (requirements.env.required/optional/one_of) when no.forge-outputis present.one_ofgroups are treated as optional so they display without causing false failures.
--skip-env-check downgrades a missing required var from a local exit-2
failure to a platform-side warning (the platform re-checks anyway).
Skill discovery
The CLI lifts display and policy metadata (name, description, category,
declared env requirements, egress domains) from the repo's skill files,
mirroring forge's own discovery rules: skills/*.md, skills/*/SKILL.md,
plus the file named by forge.yaml's skills.path (default SKILL.md).
What is read from forge.yaml
From forge.yaml the CLI lifts: agent_id (required — also the default
agent name), version, framework, model.provider / model.name,
egress.mode / egress.allowed_domains, channels, skills.path, audit
capture settings, and compression. The exact file bytes are also uploaded for
provenance. Unknown fields never break a deploy.
CI provenance
When running under GitHub Actions or GitLab CI, the CLI auto-detects provenance metadata (provider, commit SHA, run URL) from the standard CI env vars and attaches it to the deploy for display on the platform.
Metadata tags
agent.tags in the spec and repeated --tag key=value flags merge into one
tag set — the flag wins on a key collision. The platform stamps the tags onto
the agent's pods as annotations. created-by and workspace-id are reserved
keys the platform adds itself; user-supplied values for them are dropped
server-side.
The tag set sent with a deploy is authoritative: removing a tag from
agent.tags (or clearing them all) removes it from the agent on the next
deploy. A --tag entry without = or with an empty key is a usage error
(exit 2); an empty value is allowed.
Spec tags stay literal (like image and name, so what gets deployed is
auditable from the file); dynamic values come via --tag, which the shell or
CI expands:
initializ agent deploy -f initializ-deploy.yaml \
--image "$IMAGE" \
--tag "commit=$GITHUB_SHA" --tag "run=$GITHUB_RUN_ID" \
--waitRaw pod annotations
Metadata tags are namespaced by the platform, which is wrong for keys that
other tooling reads by exact name — a metrics scraper's
prometheus.io/scrape, for example. agent.annotations in the spec and
repeated --annotation key=value flags declare raw pod annotations that
the platform stamps verbatim, with no namespacing. As with tags, the flag
wins on a key collision, spec values stay literal, and the resolved set
reconciles wholesale on every deploy — removing an annotation from the spec
removes it from the agent.
Unlike tags, raw annotations are never silently coerced — the exact key is the point, so validation is fail-loud:
- Keys using a reserved prefix —
kubernetes.io/,k8s.io/,initializ.ai/, or any subdomain of them — are rejected by the platform, so a raw annotation can never overwrite a platform-owned key. - Invalid annotation keys are rejected too, and the error names all offending keys at once.
initializ agent deploy -f initializ-deploy.yaml \
--annotation "prometheus.io/scrape=true" \
--annotation "prometheus.io/port=9090" \
--waitNetwork reachability (the ingress block)
By default an agent is reachable only inside the platform's cluster. The
ingress block asks the platform to expose the agent's endpoint further out
— its A2A endpoint, or the HTTP invoke endpoint
of a non-A2A agent. It works on both runtimes (forge and claude-agent).
| Field | Default | Description |
|---|---|---|
reachability | Network scope: cluster (in-cluster only), private (internal network / VPN), or public (internet) | |
host | <agent>-<workspace>.<tier domain> | Hostname override for the exposed endpoint |
enabled | false | Deprecated — true maps to the platform's default exposure mode; use reachability instead |
className | tier-driven | Advanced per-agent routing-class override; normally the platform's exposure tier supplies it |
tls | tier-driven | Advanced override: serve the endpoint over TLS |
timeoutServer | tier-driven | Advanced override: server timeout for the exposed endpoint |
The CLI only names the mode — which modes exist, and the network, domain, and scheme each maps to, is configured per platform environment by the operator. Semantics that follow from that split:
- The CLI rejects only typos (anything other than
cluster,private,public, or empty — exit 2). Naming a mode the environment doesn't offer fails the deploy server-side with the list of allowed modes. - When the mode is offered but its hostname can't be resolved (no domain
configured and no
hostoverride), the deploy warns and falls back to cluster scope rather than failing the CI run. - The default hostname is
<agent>-<workspace>.<tier domain>;hostoverrides it. - The spec is the source of truth on every deploy: removing the
ingressblock — or settingreachability: cluster— withdraws a previously provisioned endpoint on the next deploy. - For an A2A agent, the Agent Card advertises the exposed URL when a tier is active, so off-cluster callers discover the right address; a cluster-scoped agent's card advertises its in-cluster address.
ingress:
reachability: public
# host: reviewer.example.com # optional overrideThe claude-agent runtime
agent.type: claude-agent deploys a governed Claude Agent SDK image instead
of a forge agent. The CLI reads no forge.yaml, .forge-output, or
SKILL.md — the image is governed at CI build time, and the platform wires
the model gateway, workload identity, policy enforcement, and audit pipeline
into the deployment server-side. The spec declares only the image, the model
provider, app env, egress, and sizing — never LLM credentials.
initializ agent init scaffolds a
claude-agent repo (this spec plus an A2A executor stub).
Differences from a forge deploy:
agent.nameis required (there is noforge.yamlto default it from).model.provideris required (anthropicoropenai) — it selects which platform-managed LLM gateway the platform injects; the repo and image carry no gateway URL or token.model.nameoptionally pins a model; otherwise the platform default is used. (Forge runtimes take provider/model fromforge.yaml, not from the spec'smodel:block.)- The local env-var contract check is skipped — there is no build manifest to read; the platform still validates server-side.
- The
forge:block and skill discovery do not apply. - Three spec blocks are claude-agent-only:
a2a(A2A exposure),storage/initContainers(stateful agents), andhttp(HTTP invoke). Any of them on a forge spec is a validation error (exit 2).
apiVersion: initializ.ai/v1
kind: AgentDeploy
agent:
name: code-reviewer # required — the upsert key is (workspace, name)
type: claude-agent
# The governed image built and pushed by CI. --image overrides this.
image: registry.initializ.ai/acme/code-reviewer:latest
# Selects the platform-managed LLM gateway. The repo holds no gateway URL or
# token — the platform injects them from the workspace configuration.
model:
provider: anthropic # anthropic | openai (required)
# name: claude-opus-4-8 # optional model pin; platform default otherwise
# Optionally expose the agent over A2A (Agent2Agent) — see below.
a2a:
enabled: true
auth: bearer
# App-level env only — LLM credentials do not belong here.
env:
- name: INITIALIZ_ENFORCEMENT
value: audit_only # observe-first; flip to enforce when ready
# Extra egress domains merged additively with the platform baseline.
egress:
additionalDomains:
- github.com
resources:
replicas: 1
requests: {cpu: 250m, memory: 512Mi}
deploy:
wait: true
timeout: 10mA2A exposure
The a2a block exposes the agent over the Agent2Agent protocol. The platform
opens the port and Service and injects the INITIALIZ_A2A_* environment; the
governed loader in the image serves the Agent Card
(/.well-known/agent-card.json) and the JSON-RPC endpoint, so A2A ingress is
governed. A2A is only supported for non-forge runtimes — a2a.enabled: true
on a forge spec is a validation error (exit 2).
| Field | Default | Description |
|---|---|---|
enabled | false | Expose the agent over A2A |
port | 9090 | Container port for the A2A server |
name | agent.name | Agent Card name |
description | Agent Card description | |
auth | "" | Inbound auth: bearer requires callers to present a platform token, none disables the check; empty applies the platform default (bearer) |
To reach the A2A endpoint from outside the cluster, add an
ingress block — the Agent Card
then advertises the exposed URL.
Stateful agents (storage and initContainers)
Declaring any storage volume makes the agent stateful: the
platform provisions persistent, per-replica storage mounted into the agent's
container, so working state — repository clones, caches, a worker's
scratch data — survives restarts and redeploys. Without storage, the
agent's filesystem is ephemeral.
storage[] field | Default | Description |
|---|---|---|
name | required | Volume name |
size | required | Capacity, e.g. 20Gi |
mountPath | required | Where the volume mounts in the agent container |
storageClass | platform default | Storage tier to provision from (operator-configured) |
initContainers declares setup steps that run before the agent starts —
typically to populate a storage volume (cloning a repository, warming a
cache). Each one runs with the agent's own image (unless image overrides
it), the agent's environment, and all storage volumes mounted.
initContainers require at least one storage volume.
initContainers[] field | Default | Description |
|---|---|---|
name | required | Step name |
command | required | Command to run (list form) |
image | the agent's image | Alternative image for this step |
storage:
- name: repos
size: 20Gi
mountPath: /workspace/repos
initContainers:
- name: clone
command: ["sh", "-c", "git clone https://github.com/acme/app /workspace/repos/app || true"]Like ingress, both blocks reconcile from the spec on every deploy —
removing them reverts the agent to a stateless deployment on the next
deploy.
HTTP invoke (non-A2A agents)
Some agents aren't A2A servers — they serve a plain HTTP route of their own
(a webhook receiver, a manual-trigger endpoint). The http block declares
that route so the platform recognises the agent as HTTP-invoked: the
console shows the endpoint URL and path on the agent's Settings and renders
an invoke form for it. An agent is invoked over A2A or plain HTTP, never
both — http and a2a.enabled are mutually exclusive (exit 2).
| Field | Default | Description |
|---|---|---|
path | required | The invoke route the agent's own server serves, e.g. /review/{pr}. {name} placeholders are filled from the invoke input |
method | POST | HTTP method: GET, POST, PUT, PATCH, or DELETE |
input | Optional JSON-Schema object describing the request. When present the console renders a typed form — fields matching {placeholders} fill the path, the rest form the JSON body; without it, the console offers a raw JSON body |
http:
path: /review/{pr}
method: POST
input:
type: object
properties:
pr: {type: string, description: Pull request number}
urgency: {type: string, enum: [low, high]}
required: [pr]When an HTTP-invoked agent is exposed beyond the cluster, the platform authenticates calls to the declared invoke path: callers present a platform-issued token, which the platform verifies before the call reaches the agent (any valid token of the agent's organization — a platform-minted token or an operator's own — is accepted), and each verification is recorded in the agent's audit trail. Other routes the agent serves — health checks, webhooks that carry their own verification — pass through untouched. A cluster-only HTTP agent is not fronted; its declared route is descriptive.
Waiting for the rollout
With --wait (or deploy.wait: true in the spec) the command polls the
agent's deploy status until it is terminal:
- Statuses progress through
buildingtosuccessorfailed; each transition is printed to stderr (suppressed by--quiet). - Poll interval defaults to 5s (with jitter so parallel CI jobs don't
stampede the API); override with
--poll-interval. - Timeout resolution:
--timeoutflag, else the spec'sdeploy.timeout, else 10m. On expiry the command exits with code 6. - A
failedstatus exits with code 5 and prints the server-side error (or points you atinitializ agent logs). - Transient errors (network, 5xx) are tolerated up to 5 consecutive polls; auth or not-found errors abort immediately.
Without --wait, the command prints the deploy id to stdout and returns as
soon as the platform accepts the request (the rollout continues
asynchronously).
Deploy output
Text mode (progress on stderr, result on stdout):
deploying support-agent (image registry.initializ.ai/acme/support-agent:sha-1a2b3c4) to workspace ws_a1b2c3de…
accepted: agent agt-42 deploy dep-777
status: building
status: success
deployed agt-42 (registry.initializ.ai/acme/support-agent:sha-1a2b3c4)With -o json and --wait, one JSON document goes to stdout:
{
"agent_id": "agt-42",
"deploy_id": "dep-777",
"status": "success",
"image": "registry.initializ.ai/acme/support-agent:sha-1a2b3c4",
"warnings": []
}Without --wait, -o json prints the acceptance response
(agentId, deployId, status, created, warnings); text mode prints
just the deploy id.
Complete GitHub Actions workflow
name: deploy-agent
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install forge
run: |
curl -fsSL https://github.com/initializ/forge/releases/latest/download/forge-Linux-x86_64.tar.gz \
| tar xz && sudo mv forge /usr/local/bin/
# forge build generates .forge-output/ (Dockerfile, build-manifest.json,
# k8s templates) — the CLI reads the env-var contract from it.
- name: forge build
run: forge build
- uses: docker/login-action@v3
with:
registry: registry.initializ.ai
username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- uses: docker/build-push-action@v6
with:
context: .forge-output
push: true
tags: registry.initializ.ai/acme/support-agent:sha-${{ github.sha }}
- name: Install initializ CLI
run: |
curl -fsSL https://github.com/initializ/cli/releases/latest/download/initializ_linux_amd64.tar.gz \
| tar xz && sudo mv initializ /usr/local/bin/
- name: Deploy to initializ
env:
INITIALIZ_API_URL: https://api.initializ.ai
INITIALIZ_TOKEN: ${{ secrets.INITIALIZ_TOKEN }}
INITIALIZ_ORG_ID: ${{ vars.INITIALIZ_ORG_ID }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
run: |
initializ auth whoami
initializ agent deploy -f initializ-deploy.yaml \
--image "registry.initializ.ai/acme/support-agent:sha-${{ github.sha }}" \
--wait --timeout 10mBuildkite pipelines
The same flow ports to Buildkite, with two Buildkite-specific things to watch:
- Keep
forge build, the docker build/push, andinitializ agent deployin one step — Buildkite steps don't share a workspace, and.forge-output/feeds both the docker build and the CLI's env-var contract check. To gate a production deploy, split just the deploy behind ablockstep and pass.forge-output/**between steps as a build artifact. - In
pipeline.yml, a single$is interpolated by Buildkite at pipeline-upload time — use$$for anything resolved at runtime on the agent. Read secrets (INITIALIZ_TOKEN, registry credentials, provider API keys) at runtime withbuildkite-agent secret getso they stay out of the step environment and are redacted in logs; on self-hosted agents without cluster secrets, export them from anenvironmentagent hook instead.
Pin the forge version installed in the pipeline deliberately — the forge
version installed in CI is the runtime baked into the agent image; don't
float on latest.
Buildkite with AWS ECR
When the workspace Environment's registry is Amazon ECR:
- No registry username/password secrets —
docker loginuses a 12-hour token fromaws ecr get-login-password, so the only AWS credential is the Buildkite agent's IAM identity (instance role on self-hosted agents, or a role assumed via Buildkite OIDC). - The ECR repository must exist before the push — ECR does not create
repositories on push. The platform auto-creates repositories only for
images it builds itself; a CI-built image is pushed by CI, so the pipeline
ensures the repo (
aws ecr describe-repositories … || aws ecr create-repository …). - The agent's IAM identity needs
ecr:GetAuthorizationToken, plusecr:DescribeRepositories,ecr:CreateRepository,ecr:BatchCheckLayerAvailability,ecr:InitiateLayerUpload,ecr:UploadLayerPart,ecr:CompleteLayerUpload, andecr:PutImageon the repository. - Configure the workspace Environment's registry URL as the ECR host with no static registry credentials — leftover static credentials silently take precedence over the IAM-based path.