# How CI works

> Spindles are Tangled's CI runners. They watch your knot for pushes and run the workflows in .tangled/workflows/.

Spindle is a small CI runner service. It watches the knot that hosts your repository. When you push, it runs the workflow files in `.tangled/workflows/` on that commit. You can use Tangled’s hosted spindle or [run your own](/docs/self-hosting/spindle/).

```yaml
# .tangled/workflows/test.yml
when:
  - event: push
    branch: main


engine: microvm
image: nixos


dependencies:
  - go


steps:
  - name: test
    command: go test ./...
```

[Writing workflows](/docs/spindles/writing-workflows/) explains this file one part at a time.

## The CI model

```plaintext
push ──▶ pipeline ──┬──▶ workflow (test.yml)  ──▶ step, step, step
                    └──▶ workflow (lint.yml)  ──▶ step, step
```

* One trigger event makes one **pipeline**.
* Each workflow file that matches the event becomes a **workflow** in that pipeline. Workflows run in parallel, each in its own container or microVM.
* A workflow is a list of **steps** that run in order. The first step that exits non-zero fails the workflow. The steps after it do not run.

## From push to run

```plaintext
you ──git push──▶ knot ──notifyEvent──▶ spindle ──▶ pipeline
```

1. You push to your knot.
2. The knot tells the repository’s spindle about the push. The spindle ignores refs that are not branches or tags, and pushes made with the `skip-ci` push option.
3. The spindle reads the workflow files at the pushed commit and keeps those whose `when` block matches the event, branch or tag, and changed paths. If none match, nothing runs.
4. Each matching workflow becomes a run in a pipeline. The repository’s **Pipelines** tab and the commit show its status and logs.

Behind this:

* The repository’s `org.tangled.repo.manifest` record names its spindle. When you pick a spindle in **Settings → Pipelines**, the manifest changes, and that spindle starts to serve the repository.
* The spindle watches the Jetstream for changes to repositories, collaborators, pull requests and patches. It reads the records themselves from the knot.
* Pushes and manual runs reach the spindle as an `org.tangled.event.notifyEvent` call. Only the repository’s own knot can send a push. Pull request runs start from the pull request and patch records.
* The spindle writes no records. Pipelines, statuses, logs, secrets and webhooks live in the spindle’s own database. The web app reads them with `org.tangled.ci.*` queries, and logs stream over a WebSocket.

## The engines

Spindle has two execution backends, picked per-workflow with the `engine` field:

| Engine    | Runs on                                                                                                 | Use it for                                                               |
| --------- | ------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
| `nixery`  | A fresh Docker or Podman container for each step, with the image built from your Nix packages by Nixery | Short, stateless builds and tests.                                       |
| `microvm` | The whole workflow inside its own microVM, with a choice of images                                      | Stronger isolation, NixOS services such as PostgreSQL, Docker inside CI. |

With nixery, state persists across steps only in the `/tangled/workspace` directory. With microvm, files, packages and services persist for the whole workflow. Which engines and images exist depends on the spindle operator.