Buildkite Agent — API Surface#

API types#

The agent exposes four distinct API surfaces:

  1. CLI — the primary human-facing interface (urfave/cli)
  2. gRPC/Connect streaming — inbound from Buildkite SaaS for real-time job dispatch
  3. REST client — outbound calls to the Buildkite SaaS REST API
  4. Two internal Unix-socket HTTP APIs — the Job API (job ↔ executor) and the Agent API (agent ↔ agent, for distributed locking)

Additionally, the agent exposes a minimal health/metrics HTTP server for orchestrators.


CLI#

Framework#

github.com/urfave/cli v1 (not Cobra). Commands are declared as cli.Command structs with inline Flags slices and an Action function.

Global flags#

Two shared flag sets are composed into every command:

  • globalFlags()--no-color, --debug, --log-format, --experiment
  • apiFlags()--agent-access-token / BUILDKITE_AGENT_ACCESS_TOKEN, --endpoint / BUILDKITE_AGENT_API_ENDPOINT

Env-var binding is handled automatically by urfave/cli via the EnvVar field on each cli.Flag. Config file values (INI format) are overlaid via the cliconfig package.

Command structure#

buildkite-agent
├── start                  (daemon: register + poll/stream for jobs)
├── bootstrap              (internal: execute a single job as subprocess)
├── kubernetes-bootstrap   (internal: k8s sidecar variant of bootstrap)
├── acknowledgements       (print OSS license acknowledgements)
├── annotate               (create/update build annotation)
├── annotation
│   └── remove             (delete a build annotation)
├── artifact
│   ├── upload             (upload files to Buildkite artifact store)
│   ├── download           (download artifacts from a build)
│   ├── search             (search artifacts with glob patterns)
│   └── shasum             (verify artifact checksum)
├── build
│   └── cancel             (cancel a running build)
├── cache                  (hidden/experimental)
│   ├── save
│   └── restore
├── env
│   ├── dump               (print all env vars for current job)
│   ├── get                (get a single env var)
│   ├── set                (set env vars via the Job API)
│   └── unset              (unset env vars via the Job API)
├── git-credentials        (git credential helper integration)
├── job
│   └── update             (update job attributes)
├── lock
│   ├── acquire            (acquire a named distributed lock)
│   ├── do                 (acquire lock, run command, release)
│   ├── done               (release a lock by token)
│   ├── get                (get current lock state)
│   └── release            (release a lock)
├── meta-data
│   ├── set                (set a key-value pair on the build)
│   ├── get                (get a value by key)
│   ├── exists             (check if a key exists)
│   └── keys               (list all metadata keys)
├── oidc
│   └── request-token      (request an OIDC token for the current job)
├── pause                  (pause the agent from accepting new jobs)
├── pipeline
│   └── upload             (upload a pipeline YAML definition)
├── redactor
│   └── add                (add a new secret pattern to the log redactor)
├── resume                 (resume a paused agent)
├── secret
│   └── get                (fetch a pipeline secret from Buildkite)
├── step
│   ├── get                (get a step attribute)
│   ├── update             (update a step attribute)
│   └── cancel             (cancel all jobs for a step)
├── stop                   (stop the agent after current job finishes)
└── tool
    ├── keygen             (generate a signing key pair)
    └── sign               (sign a pipeline with a key)

Flag patterns#

  • Persistent globals via slices.Concat(globalFlags(), apiFlags(), [...command-specific...]) — no cobra-style PersistentFlags, each command just concatenates the shared slices.
  • Struct-tag decoding: flags are decoded into typed config structs (AgentStartConfig, BootstrapConfig, ArtifactUploadConfig, etc.) via a setupLoggerAndConfig[T]() helper using reflection + the cli:"flag-name" struct tag.
  • Normalization tags: normalize:"filepath|list|commandpath" on struct fields triggers path expansion, comma-splitting, or exec.LookPath before use.

gRPC / Connect API (inbound streaming)#

The agent acts as a client to a gRPC service hosted by Buildkite SaaS, using connectrpc.com/connect (gRPC-over-HTTP/1.1+HTTP/2).

Proto definition#

api/proto/agentedge.proto

Service#

service AgentEdgeService {
  rpc StreamPings(StreamPingsRequest) returns (stream StreamPingsResponse) {}
}

The agent sends its agent_id and receives a server-side stream of action messages. Each StreamPingsResponse carries one of:

  • ResumeAction — resume after pause
  • PauseAction { reason } — pause job acceptance
  • DisconnectAction { reason } — graceful disconnect
  • JobAssignedAction { job { id } } — new job available

Client usage#

api/pings_streaming.go wraps the generated Connect client. The AgentWorker.runStreamingPingLoop() receives these messages and sends them to the internal actionMessage channel (same channel as the HTTP ping loop). A baton primitive ensures only one loop is “active” at a time.

Interceptors / Auth#

The Connect client sends the agent access token as a Bearer token in the Authorization header, same as REST calls. No additional gRPC interceptors are registered.


REST API (outbound — to Buildkite SaaS)#

The api package is a typed REST client against the Buildkite Agent API (default endpoint: https://agent.buildkite.com/v3/). Authentication is via Authorization: Token <agent-access-token> header.

Client construction#

api.NewClient(conf ClientConfig) — takes a token, endpoint, TLS config, and optional HTTP transport overrides. The retryable.go wraps the transport with automatic retry logic.

Endpoint groups#

Agent lifecycle:

MethodPathDescription
POSTregisterRegister agent with Buildkite, returns access token
POSTconnectMark agent as connected
POSTdisconnectMark agent as disconnected
POSTstopRequest agent stop (graceful/forced)
POSTpausePause job acceptance
POSTresumeResume from pause
POSTheartbeatSend keepalive heartbeat
GETpingPoll for pending jobs

Job execution:

MethodPathDescription
POSTjobs/{id}/acceptAccept a job assignment
PUTjobs/{id}/startSignal job has started
PUTjobs/{id}/finishReport job completion + exit code
GETjobs/{id}/cancel (polling)Check if job was cancelled

Log streaming:

MethodPathDescription
POSTjobs/{id}/chunksUpload log chunk (multipart form)
POSTjobs/{id}/header_timesUpload section timing data

Artifacts:

MethodPathDescription
POSTbuilds/{id}/artifactsCreate artifact batch
PUT(S3/GCS/Azure URL)Upload artifact content (direct to storage)
GETbuilds/{id}/artifactsSearch artifacts
PUTartifact state update endpointMark artifacts as uploaded

Pipeline / metadata / annotations / steps:

MethodPathDescription
POSTpipelines/{id}/uploadUpload pipeline YAML
GETpipelines/{id}Fetch pipeline
POSTjobs/{id}/dataSet metadata key
POSTjobs/{id}/data/getGet metadata value
POSTjobs/{id}/data/existsCheck metadata key
POSTjobs/{id}/data/keysList metadata keys
POSTjobs/{id}/annotationsCreate/update build annotation
DELETEjobs/{id}/annotations/{style}Delete annotation
GETjobs/{id}/steps/{step_uuid}Get step attributes
PUTjobs/{id}/steps/{step_uuid}Update step attributes
POSTbuilds/{id}/cancelCancel a build

Secrets / OIDC:

MethodPathDescription
GETjobs/{id}/secretsFetch pipeline secrets
POSTjobs/{id}/oidc/tokensRequest OIDC token

Authentication#

Bearer token (agent registration token initially; swapped for per-agent access token after register). Passed via Authorization: Token <value> header.


Internal HTTP APIs (Unix domain socket)#

Both internal APIs use chi as the router and internal/socket for transport + auth middleware. They listen on Unix domain sockets, not TCP, so they are not network-accessible.

Job API (jobapi package)#

Purpose: Allows a running job (its hooks, plugins, user scripts) to introspect and mutate its own environment during execution.

Socket path: Set via BUILDKITE_AGENT_JOB_API_SOCKET env var; token via BUILDKITE_AGENT_JOB_API_TOKEN.

Middleware chain:
LoggerMiddlewaremiddleware.RecovererHeadersMiddleware (Content-Type: application/json) → AuthMiddleware (Bearer token)

Routes (/api/current-job/v0):

MethodPathDescription
GET/envDump current job environment
PATCH/envSet/modify environment variables
DELETE/envUnset environment variables
POST/redactionsRegister new secret patterns to redact from logs

Auth: Bearer token generated at server start via socket.GenerateToken(32), injected into the bootstrap subprocess environment as BUILDKITE_AGENT_JOB_API_TOKEN.

Client: jobapi/client.go wraps the socket transport; env get/set/unset and redactor add CLI commands use this client to communicate with the running server.


Agent API (internal/agentapi package)#

Purpose: Provides distributed locking across multiple agent workers (same host or shared filesystem), used by buildkite-agent lock commands.

Socket path: Set via BUILDKITE_AGENT_SOCKET_SOCK env var (leader agent’s socket).

Middleware chain:
LoggerMiddleware (debug-only) → middleware.RecovererHeadersMiddleware (Content-Type: application/json)

Note: Unlike the Job API, the Agent API does not use AuthMiddleware — access is controlled by filesystem permissions on the Unix socket.

Routes (/api/leader/v0):

MethodPathDescription
GET/pingHealth check, returns current time
GET/lockGet current lock state for a resource
PATCH/lockAcquire or release a lock (CAS semantics)

Client: internal/agentapi/client.go; used by lock acquire/do/done/get/release CLI commands.


Health / Metrics HTTP server#

The AgentPool optionally starts a public HTTP server (TCP, not Unix socket) for orchestration platforms. Enabled via --metrics-datadog flag or when a --status-bind-address is configured.

Router: stdlib http.NewServeMux

Routes:

MethodPathDescription
GET/Health handler (200 OK)
GET/metricsPrometheus metrics (via promhttp.Handler())
GET/statusHTML status page
GET/status.jsonJSON status (worker states, job counts)
GET/agent/{N}Per-worker health handler

Plugin / Extension system#

Buildkite has a rich plugin model, but plugins are not loaded in-process — they are external repositories fetched and executed as shell scripts. The extension points are:

  • --plugins flag / BUILDKITE_PLUGINS env var: JSON-encoded list of plugin specs; each plugin is a Git repo + config
  • Plugin hooks: Shell scripts in the plugin repo (pre-checkout, post-checkout, pre-command, post-command, etc.) discovered by internal/job/hook.FindAll() and executed by internal/shell.Shell at each Executor phase
  • Agent hooks directory (--hooks-path / BUILDKITE_HOOKS_PATH): Local hooks that wrap every job, regardless of the pipeline
  • --bootstrap-script: Replace the entire Executor with a custom script (escape hatch)

The plugin interface is purely file-system and environment-variable based — no in-process plugin API. This keeps the agent language-agnostic: plugins can be written in any language.


Key API design observations#

  1. Three tiers of API consumers: (a) Buildkite SaaS controls the agent via the REST + gRPC APIs; (b) jobs/hooks interact with the running executor via Unix-socket Job API; (c) co-located agents coordinate via Unix-socket Agent API. Each tier has its own auth model and transport.

  2. Unix sockets as security boundary: Both internal APIs use Unix domain sockets, making network-based access impossible. The Job API additionally uses a per-server random Bearer token; the Agent API relies on OS file permissions.

  3. CLI is the primary user surface: Almost all user-facing operations (artifact management, metadata, pipeline upload, secrets, OIDC, step control) are exposed as CLI subcommands that call the SaaS REST API directly. The agent is not just a daemon; it is a rich CI toolkit.

  4. REST API is entirely outbound: The agent never opens an inbound TCP port for the Buildkite SaaS connection — all communication is agent-initiated (poll or long-lived stream). This simplifies firewall rules for self-hosted agents.

  5. gRPC via Connect, not native gRPC: The streaming ping path uses connectrpc.com/connect which tunnels gRPC over standard HTTP/1.1 or HTTP/2, avoiding the need for a gRPC-specific proxy or load balancer at the customer’s network boundary.