Buildkite Agent — API Surface#
API types#
The agent exposes four distinct API surfaces:
- CLI — the primary human-facing interface (urfave/cli)
- gRPC/Connect streaming — inbound from Buildkite SaaS for real-time job dispatch
- REST client — outbound calls to the Buildkite SaaS REST API
- 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,--experimentapiFlags()—--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-stylePersistentFlags, each command just concatenates the shared slices. - Struct-tag decoding: flags are decoded into typed config structs (
AgentStartConfig,BootstrapConfig,ArtifactUploadConfig, etc.) via asetupLoggerAndConfig[T]()helper using reflection + thecli:"flag-name"struct tag. - Normalization tags:
normalize:"filepath|list|commandpath"on struct fields triggers path expansion, comma-splitting, orexec.LookPathbefore 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 pausePauseAction { reason }— pause job acceptanceDisconnectAction { reason }— graceful disconnectJobAssignedAction { 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:
| Method | Path | Description |
|---|---|---|
| POST | register | Register agent with Buildkite, returns access token |
| POST | connect | Mark agent as connected |
| POST | disconnect | Mark agent as disconnected |
| POST | stop | Request agent stop (graceful/forced) |
| POST | pause | Pause job acceptance |
| POST | resume | Resume from pause |
| POST | heartbeat | Send keepalive heartbeat |
| GET | ping | Poll for pending jobs |
Job execution:
| Method | Path | Description |
|---|---|---|
| POST | jobs/{id}/accept | Accept a job assignment |
| PUT | jobs/{id}/start | Signal job has started |
| PUT | jobs/{id}/finish | Report job completion + exit code |
| GET | jobs/{id}/cancel (polling) | Check if job was cancelled |
Log streaming:
| Method | Path | Description |
|---|---|---|
| POST | jobs/{id}/chunks | Upload log chunk (multipart form) |
| POST | jobs/{id}/header_times | Upload section timing data |
Artifacts:
| Method | Path | Description |
|---|---|---|
| POST | builds/{id}/artifacts | Create artifact batch |
| PUT | (S3/GCS/Azure URL) | Upload artifact content (direct to storage) |
| GET | builds/{id}/artifacts | Search artifacts |
| PUT | artifact state update endpoint | Mark artifacts as uploaded |
Pipeline / metadata / annotations / steps:
| Method | Path | Description |
|---|---|---|
| POST | pipelines/{id}/upload | Upload pipeline YAML |
| GET | pipelines/{id} | Fetch pipeline |
| POST | jobs/{id}/data | Set metadata key |
| POST | jobs/{id}/data/get | Get metadata value |
| POST | jobs/{id}/data/exists | Check metadata key |
| POST | jobs/{id}/data/keys | List metadata keys |
| POST | jobs/{id}/annotations | Create/update build annotation |
| DELETE | jobs/{id}/annotations/{style} | Delete annotation |
| GET | jobs/{id}/steps/{step_uuid} | Get step attributes |
| PUT | jobs/{id}/steps/{step_uuid} | Update step attributes |
| POST | builds/{id}/cancel | Cancel a build |
Secrets / OIDC:
| Method | Path | Description |
|---|---|---|
| GET | jobs/{id}/secrets | Fetch pipeline secrets |
| POST | jobs/{id}/oidc/tokens | Request 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:LoggerMiddleware → middleware.Recoverer → HeadersMiddleware (Content-Type: application/json) → AuthMiddleware (Bearer token)
Routes (/api/current-job/v0):
| Method | Path | Description |
|---|---|---|
| GET | /env | Dump current job environment |
| PATCH | /env | Set/modify environment variables |
| DELETE | /env | Unset environment variables |
| POST | /redactions | Register 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.Recoverer → HeadersMiddleware (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):
| Method | Path | Description |
|---|---|---|
| GET | /ping | Health check, returns current time |
| GET | /lock | Get current lock state for a resource |
| PATCH | /lock | Acquire 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:
| Method | Path | Description |
|---|---|---|
| GET | / | Health handler (200 OK) |
| GET | /metrics | Prometheus metrics (via promhttp.Handler()) |
| GET | /status | HTML status page |
| GET | /status.json | JSON 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:
--pluginsflag /BUILDKITE_PLUGINSenv 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 byinternal/job/hook.FindAll()and executed byinternal/shell.Shellat eachExecutorphase - Agent hooks directory (
--hooks-path/BUILDKITE_HOOKS_PATH): Local hooks that wrap every job, regardless of the pipeline --bootstrap-script: Replace the entireExecutorwith 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#
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.
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.
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.
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.
gRPC via Connect, not native gRPC: The streaming ping path uses
connectrpc.com/connectwhich 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.