# Pipelines and triggers

> Define what starts a pipeline, which jobs it calls, and how runs are limited.

> **Info:** Inngest CI is an Inngest Labs project: early, moving fast, and shaped by your feedback. What's Labs?

A pipeline is one Inngest function. Its handler is plain TypeScript that calls [jobs](/docs-markdown/labs/ci/jobs?ref=docs-labs-ci-pipelines), so it can branch, loop, and run jobs in parallel. Inngest starts a run when one of its triggers fires.

## Define a pipeline

```typescript {{ title: "ci/pipelines.ts" }}
import { github } from "@inngest/ci";
import { ci } from "./client";

export const pr = ci.pipeline(
  {
    id: "pr",
    on: github.pullRequest(),
  },
  async () => {
    await Promise.all([lint(), test()]);
  },
);
```

The `id` names the function, the run, and the check. The handler receives `event`, `events`, `runId`, `pipelineId`, `repo`, `attempt`, and `logger`. Serve pipelines with `ci.functions()`, as the [quick start](/docs-markdown/labs/ci/quick-start?ref=docs-labs-ci-pipelines#4-serve-the-pipelines) shows.

## Triggers

A trigger starts a pipeline and types its `event`. Pass an array to use several.

```typescript {{ title: "TypeScript" }}
export const merged = ci.pipeline(
  {
    id: "merged",
    on: github.pullRequest({ types: ["closed"] }),
  },
  async ({ event }) => {
    if (event.data.pull_request.merged === false) {
      return ci.skip("closed without merging");
    }

    await release();
  },
);
```

- `github.pullRequest()` and `github.push()` cover pull requests and commits.
- `github.comment()` runs on a comment such as `/deploy`, and `github.mergeGroup()` runs for the merge queue.
- `{ cron }` runs on a schedule, and `ci.manual()` runs on an event you send.

[Reference](/docs-markdown/labs/ci/reference?ref=docs-labs-ci-pipelines#triggers) lists every trigger and its options.

## Skip a run

Return `ci.skip(reason)` to end a run early. The check completes as success with the reason, so a required check never waits. `changed()` reads the changed files before any machine starts:

```typescript {{ title: "TypeScript" }}
const touched = await changed("src/**", "package.json");

if (touched === false) {
  return ci.skip("nothing that affects the build changed");
}
```

When `changed()` cannot tell what changed, it returns `true` so nothing is skipped by mistake. That happens when the run has no repository or no GitHub credentials, when it has no pull request or push range (a cron, a manual run, a merge group, or a push that creates a branch), when a local run cannot find its base branch, and when a compare hits GitHub's 300-file cap.

## Flow control

Pipeline options accept every Inngest [flow control](/docs-markdown/durable-execution/flow-control?ref=docs-labs-ci-pipelines) setting. To cancel a run when a newer one starts for the same pull request, use `singleton`:

```typescript {{ title: "TypeScript" }}
export const pr = ci.pipeline(
  {
    id: "pr",
    on: github.pullRequest(),
    singleton: { key: "event.data.pull_request.number", mode: "cancel" },
    concurrency: {
      key: "event.data.repository.owner.login",
      limit: 20,
      scope: "account",
    },
  },
  async () => {
    await test();
  },
);
```

Use `concurrency` to cap how many pipelines run at once. `throttle`, `rateLimit`, `debounce`, and `priority` work too.

## Next steps

- [Jobs](/docs-markdown/labs/ci/jobs?ref=docs-labs-ci-pipelines) covers the units of work a pipeline calls.
- [Checks and reports](/docs-markdown/labs/ci/checks-and-reports?ref=docs-labs-ci-pipelines) covers how a pipeline reports to GitHub.
- [Reference](/docs-markdown/labs/ci/reference?ref=docs-labs-ci-pipelines#ci-pipeline) lists every pipeline option.