INTEGRITY Cloudflare Docs

Build and deploy Artifacts repos

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:

How it works

Artifacts repo changes
Repository changes
CI Workflow
1Install & cache dependencies
2Run CI steps
Build
Lint
Typecheck
Format
Deploy Worker
  1. Push repo changes — A git push to the Artifacts repo emits an artifacts.repo.pushed event that identifies the pushed repo, branch, and commit.
  2. 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.
  3. Deploy the Worker — The Workflow deploys the built Worker either directly to your account or as a User Worker, if using Workers for Platforms

Run the CI Workflow

Use the @cloudflare/ci SDK to define the CI steps. The SDK provides two tools to help you build the pipeline:

Diagram showing three sequential commits: commit 1 has a cache miss so the install step runs and its sandbox snapshot is cached; commit 2 has an unchanged pnpm-lock.yaml so the cache key matches and the cached snapshot is served, skipping install; commit 3 has a changed pnpm-lock.yaml so the cache key misses and install runs 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:

src/index.js
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,
			},
		});
	}
}
src/index.ts
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,
			},
		});
	}
}

Start a build when code changes

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:

{
  "$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 = true

View build status

The [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:

Go to Workflows ↗

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.

Deploy the application

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.

Run one workflow for every repo in a namespace

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.

Platform namespace
Artifact repo A
Artifact repo B
Artifact repo C
Artifact repo D
Artifact repo E
Shared CI workflowauthored & owned by platform
buildlinttesttypecheck
deploy A
deploy B
deploy C
deploy D
deploy E
deploy per repo

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.