initializdocs
Cli

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.
  • --image overrides the spec's image, so CI-templated tags need no yq/sed rewriting of the manifest.
  • --tag key=value adds metadata tags stamped onto the agent's pods as annotations (repeatable; overrides the spec's agent.tags) — handy for injecting $GITHUB_SHA and the like.
  • --annotation key=value stamps a raw pod annotation verbatim — for keys that must keep their exact name, like prometheus.io/scrape=true (repeatable; overrides the spec's agent.annotations).
  • With --wait the command polls until the rollout finishes — exit code 5 on failure, 6 on timeout. This is the CI-friendly mode.

agent deploy flags

FlagShorthandDefaultDescription
--file-finitializ-deploy.yamlPath to the deploy spec
--imageImage ref to deploy (overrides the spec's image)
--tagMetadata tag key=value stamped as a pod annotation (repeatable; overrides the spec's agent.tags)
--annotationRaw pod annotation key=value stamped verbatim (repeatable; overrides the spec's agent.annotations; reserved prefixes are rejected)
--waitfalsePoll until the rollout finishes
--timeout0 (→ 10m)Max time to wait (Go duration, e.g. 15m)
--poll-interval0 (→ 5s)Status poll interval
--skip-env-checkfalseDon'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: 10m

Validation 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 marked optional: true, in which case it is omitted from the request entirely.
  • $$ escapes a literal $; a bare $ (not followed by {) is literal.
  • secret: true routes 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:

  1. .forge-output/build-manifest.json — the env_required / env_optional lists (structured; present in recent forge versions).
  2. .forge-output/k8s/secrets.yaml — the stringData: keys; a key preceded by a # optional comment line counts as optional (older forge versions).
  3. SKILL.md frontmatter (requirements.env.required / optional / one_of) when no .forge-output is present. one_of groups 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" \
  --wait

Raw 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 prefixkubernetes.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" \
  --wait

Network 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).

FieldDefaultDescription
reachabilityNetwork scope: cluster (in-cluster only), private (internal network / VPN), or public (internet)
host<agent>-<workspace>.<tier domain>Hostname override for the exposed endpoint
enabledfalseDeprecatedtrue maps to the platform's default exposure mode; use reachability instead
classNametier-drivenAdvanced per-agent routing-class override; normally the platform's exposure tier supplies it
tlstier-drivenAdvanced override: serve the endpoint over TLS
timeoutServertier-drivenAdvanced 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 host override), the deploy warns and falls back to cluster scope rather than failing the CI run.
  • The default hostname is <agent>-<workspace>.<tier domain>; host overrides it.
  • The spec is the source of truth on every deploy: removing the ingress block — or setting reachability: 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 override

The 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.name is required (there is no forge.yaml to default it from).
  • model.provider is required (anthropic or openai) — it selects which platform-managed LLM gateway the platform injects; the repo and image carry no gateway URL or token. model.name optionally pins a model; otherwise the platform default is used. (Forge runtimes take provider/model from forge.yaml, not from the spec's model: 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), and http (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: 10m

A2A 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).

FieldDefaultDescription
enabledfalseExpose the agent over A2A
port9090Container port for the A2A server
nameagent.nameAgent Card name
descriptionAgent 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[] fieldDefaultDescription
namerequiredVolume name
sizerequiredCapacity, e.g. 20Gi
mountPathrequiredWhere the volume mounts in the agent container
storageClassplatform defaultStorage 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[] fieldDefaultDescription
namerequiredStep name
commandrequiredCommand to run (list form)
imagethe agent's imageAlternative 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).

FieldDefaultDescription
pathrequiredThe invoke route the agent's own server serves, e.g. /review/{pr}. {name} placeholders are filled from the invoke input
methodPOSTHTTP method: GET, POST, PUT, PATCH, or DELETE
inputOptional 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 building to success or failed; 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: --timeout flag, else the spec's deploy.timeout, else 10m. On expiry the command exits with code 6.
  • A failed status exits with code 5 and prints the server-side error (or points you at initializ 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 10m

Buildkite pipelines

The same flow ports to Buildkite, with two Buildkite-specific things to watch:

  • Keep forge build, the docker build/push, and initializ agent deploy in 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 a block step 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 with buildkite-agent secret get so they stay out of the step environment and are redacted in logs; on self-hosted agents without cluster secrets, export them from an environment agent 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 login uses a 12-hour token from aws 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, plus ecr:DescribeRepositories, ecr:CreateRepository, ecr:BatchCheckLayerAvailability, ecr:InitiateLayerUpload, ecr:UploadLayerPart, ecr:CompleteLayerUpload, and ecr:PutImage on 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.

On this page