Artifacts events can build and deploy projects stored in Artifacts repos. When a user or agent pushes a commit, the event triggers a Workflow instance.
Within the Workflow, you define a continuous integration (CI) pipeline with the @cloudflare/ci SDK to cache dependencies, run checks, and build the project. The final step in your CI pipeline can deploy the output to a Worker or a Workers for Platforms User Worker.
This is useful when you need to:
- Automatically build and deploy application code stored in Artifacts.
- Run linting, type checking, tests, and other checks on every push.
- Reuse dependencies when the lockfile (i.e.
pnpm-lock.yaml) has not changed. - Stop deployment when a check or build fails.
- Restrict API token access to the deployment step.
- Deploy the output to a Worker or a Workers for Platforms User Worker.
- Push repo changes — A
git pushto the Artifacts repo emits anartifacts.repo.pushedevent that identifies the pushed repo, branch, and commit. - Run the CI Workflow — The event starts a Workflow that checks out the commit, installs and caches dependencies, and runs CI steps — build, lint, typecheck, and format — in parallel. A failed step stops the Workflow before deployment.
- Deploy the Worker — The Workflow deploys the built Worker either directly to your account or as a User Worker, if using Workers for Platforms
Use the @cloudflare/ci SDK to define the CI steps. The SDK provides two tools to help you build the pipeline:
- Runners — each
runner()call spins up an isolated sandbox and executes a shell command. You use the same commands you already run locally or in another CI system. Each runner captures its own logs, status, and output files. - Cache — the
cacheoption on a runner caches installed dependencies so that later runs do not reinstall them. Pass the files that determine the dependencies, such aspnpm-lock.yaml, tocache.inputs. When those files have not changed, the SDK restores the cached result instead of running the command again.
A cached runner takes a snapshot of its sandbox, which later runners reuse. Multiple runners can branch from the same cached result — for example, lint, type-check, and test runners can all reuse one cached install.
A failed runner retries according to its step configuration, where you can define the number of retry attempts, backoff schedule, and timeouts. Runners that depend on a previous step do not start until its retry succeeds. If the configured retry limit is reached, the Workflow terminates in an Errored state.
Here is an example of how to set up your Workflow to use runners and cache to install dependencies, run checks, build the project, and deploy the Worker:
import { CIWorkflow } from "./src/pipeline";
export class CI extends CIWorkflow {
async pipeline(_event, _step, ci) {
// Install once, then run independent checks from the shared snapshot.
const deps = await ci.runner({
name: "install",
command: "bun install --frozen-lockfile",
cache: { inputs: ["package.json", "bun.lock"] },
});
await Promise.all([
deps.runner({ name: "lint", command: "bun run lint" }),
deps.runner({ name: "test", command: "bun run test" }),
deps.runner({ name: "typecheck", command: "bun run typecheck" }),
deps.runner({ name: "build", command: "bun run build" }),
]);
await deps.runner({
name: "deploy",
command: "bun wrangler deploy",
cloudflareCredentials: {
accountId: this.env.CLOUDFLARE_DEPLOY_ACCOUNT_ID,
},
});
}
}import { CIWorkflow } from "./src/pipeline";
import type {
CiContext,
CiParams,
CiRunnerResult,
CloudflareArtifacts,
} from "./src/pipeline";
import type { WorkflowEvent, WorkflowStep } from "cloudflare:workers";
export class CI extends CIWorkflow {
protected async pipeline(
_event: WorkflowEvent<CiParams<CloudflareArtifacts>>,
_step: WorkflowStep,
ci: CiContext,
): Promise<void> {
// Install once, then run independent checks from the shared snapshot.
const deps: CiRunnerResult = await ci.runner({
name: "install",
command: "bun install --frozen-lockfile",
cache: { inputs: ["package.json", "bun.lock"] },
});
await Promise.all([
deps.runner({ name: "lint", command: "bun run lint" }),
deps.runner({ name: "test", command: "bun run test" }),
deps.runner({ name: "typecheck", command: "bun run typecheck" }),
deps.runner({ name: "build", command: "bun run build" }),
]);
await deps.runner({
name: "deploy",
command: "bun wrangler deploy",
cloudflareCredentials: {
accountId: this.env.CLOUDFLARE_DEPLOY_ACCOUNT_ID,
},
});
}
}When a user or agent pushes a commit to an Artifacts repo, Artifacts emits an event that identifies the repo, branch, and commit that changed. You will use this event to trigger a Workflow instance which runs a CI pipeline by automatically checking out the commit and cloning the repo before installing dependencies, running checks, building the project, and/or deploying the Worker, according to your code.
This guide defines those CI steps in a Workflow class named CIWorkflow. To start this Workflow automatically after each push, add an cf.artifacts.repo.pushed trigger to your Wrangler configuration. You should also include:
- R2 binding: the bucket where your the snapshot of your cached dependencies will be stored
- Container (and Durable Object) binding: create a binding to your container to access sandboxes during each
runner()step - Workflows binding
- Artifacts binding
- Observability (optional): inspect your CI jobs as a Workflow instance with Workers observability
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "<worker-name>",
"main": "src/index.ts",
"compatibility_date": "2026-06-16",
"compatibility_flags": ["nodejs_compat"],
"artifacts": [
{
"binding": "ARTIFACTS",
"namespace": "<artifacts-namespace>"
}
],
"containers": [
{
"class_name": "CiSandbox",
"image": "./Dockerfile",
"max_instances": 10,
"instance_type": "standard-4"
}
],
"durable_objects": {
"bindings": [
{
"name": "SANDBOX",
"class_name": "CiSandbox"
}
]
},
"workflows": [
{
"name": "<workflow-name>",
"binding": "CI_WORKFLOW",
"class_name": "CI"
}
],
"exports": {
"CiSandbox": {
"type": "durable-object",
"storage": "sqlite"
}
},
"r2_buckets": [
{
"binding": "BACKUP_BUCKET",
"bucket_name": "<backup-bucket-name>"
}
],
"triggers": {
"events": [
{
"type": "cf.artifacts.repo.pushed",
// filter is optional. If you don't set repoName we will run the same workflow for every push on any repo in your Artifacts namespace
"filter": {
"namespace": "CI",
"repoName": "my-repo"
},
"target": {
"scriptName": "<worker-name>",
"workflowName": "<workflow-name>"
}
}
]
},
"observability": {
"enabled": true,
"logs": {
"enabled": true
}
}
}"$schema" = "node_modules/wrangler/config-schema.json"
name = "<worker-name>"
main = "src/index.ts"
compatibility_date = "2026-06-16"
compatibility_flags = [ "nodejs_compat" ]
[[artifacts]]
binding = "ARTIFACTS"
namespace = "<artifacts-namespace>"
[[containers]]
class_name = "CiSandbox"
image = "./Dockerfile"
max_instances = 10
instance_type = "standard-4"
[[durable_objects.bindings]]
name = "SANDBOX"
class_name = "CiSandbox"
[[workflows]]
name = "<workflow-name>"
binding = "CI_WORKFLOW"
class_name = "CI"
[exports.CiSandbox]
type = "durable-object"
storage = "sqlite"
[[r2_buckets]]
binding = "BACKUP_BUCKET"
bucket_name = "<backup-bucket-name>"
[[triggers.events]]
type = "cf.artifacts.repo.pushed"
[triggers.events.filter]
namespace = "CI"
repoName = "my-repo"
[triggers.events.target]
scriptName = "<worker-name>"
workflowName = "<workflow-name>"
[observability]
enabled = true
[observability.logs]
enabled = trueThe [observability] setting in your Wrangler configuration records the status and logs for each pipeline run. To identify which stage failed, inspect the instance in the Workflows dashboard:
Each runner displays its own input, output, and status, so you can identify the command that failed. When a runner fails, the Workflow records its output and does not start stages that need its files.
To deploy a Worker, pass wrangler deploy to your final runner() step, i.e. workspace.runner({ name: "deploy", command: "wrangler deploy" }).
To deploy a User Worker, pass wrangler deploy --dispatch_namespace <DISPATCH_NAMESPACE> to your final runner() step.
The filter in your trigger is optional. When you set repoName, only pushes to that specific repo start the Workflow. When you omit repoName, Cloudflare runs the same Workflow for every push to any repo in your Artifacts namespace.
This is useful for platforms that author and own a single CI Workflow and want to apply it uniformly across every customer repo in a namespace. Instead of maintaining a separate trigger per repo, one shared Workflow builds, checks, and deploys each repo on push.
To run the same Workflow for every repo in a namespace, drop repoName from the trigger filter and keep only the namespace:
{
"triggers": {
"events": [
{
"type": "cf.artifacts.repo.pushed",
"filter": {
"namespace": "CI"
},
"target": {
"scriptName": "my-ci-worker",
"workflowName": "ci-workflow"
}
}
]
}
}[[triggers.events]]
type = "cf.artifacts.repo.pushed"
[triggers.events.filter]
namespace = "CI"
[triggers.events.target]
scriptName = "my-ci-worker"
workflowName = "ci-workflow"Each push still starts its own Workflow instance for the repo, branch, and commit that changed, so you can deploy a separate Worker per repo from the same shared Workflow definition.