# Your repo

> Part of the Cowliss docs. Markdown for every page: append `index.md` to its URL. Site map: /llms.txt

Automations, email templates and segments are files in **your** repo: an automation is a JSON definition, a template Liquid HTML or a markdown doc. You check them locally, push them, and turn each automation on when you want it running. Nothing about them lives in Cowliss's own source tree.

What Cowliss runs is what your app holds on the server, and a push is one of the ways to write it. The dashboard, an API call and MCP write the same automations, templates, segments, event list and purposes, and `cow pull` writes whatever the app holds back into the tree as files. A push never quietly undoes one of those edits: it names it first, as a [conflict](#conflicts).

One repo pushes to **one app**. The app is the product your users belong to, and everything the repo ships (automations, templates, the segments its triggers own, the consent purposes it declares) belongs to that app along with the profiles and events already under it. `cow.json` names it, and that is the only place it is written down.

The whole loop is one CLI:

```sh
npm install --save-dev @cowliss/cli

npx cow login          # once, per machine
npx cow init          # writes .cowliss/ and asks which app
cd .cowliss
npm install
npx cow add blueprint saas-trial
npx cow test           # every scenario, on a virtual clock
npx cow plan           # what a push would change, and change nothing
npx cow push
npx cow enable trial-welcome
```

`cow init` reads your organization's apps and writes the one it resolved into `cow.json`: your only app if you have one, otherwise the one you pick. `--app app_…` answers it without being asked, and an organization with no app at all is a refusal, because `cow init` never creates one: an app named after whatever the directory happened to be called would be a fifth product nobody meant to have. Create the app first, then run `cow init` again.

It also accepts `--blueprint <name>` to start from a gallery blueprint instead of the example it otherwise writes (`automations/welcome.json`, its emails and its scenario, plus an `AGENTS.md` for coding agents), and `--org org_…` when there is no session to read the org from (CI). `cow add blueprint` refuses to overwrite a file you already have. `cow init` is blunter: it stops when `.cowliss/` already holds a `cow.json`, and otherwise writes its `package.json`, `tsconfig.json`, and `.gitignore` over whatever is there.

Everything Cowliss reads and writes is in that one directory, so `cow init` runs in the repo your product already lives in: your app's tree stays as it is and `.cowliss/` sits beside it. `cow init <dir>` puts it in another repo instead. Every command finds the project from the repo root, and running one from inside `.cowliss/` works too.

## The layout

```text
my-app/               your repo, whatever is already in it
  .cowliss/
    cow.json          which org, and which app this repo pushes to
    package.json      depends on @cowliss/cli, plus react and @react-email/components for .tsx templates
    tsconfig.json     extends @cowliss/cli/tsconfig.json
    automations/
      abandoned-checkout.json     key: "abandoned-checkout"
      commerce/vip-order.json     key: "vip-order"
    emails/
      abandoned-checkout.html     key: "abandoned-checkout", Liquid HTML
      trial-ending.md             key: "trial-ending", a doc
      order-shipped.tsx           key: "order-shipped", rendered to Liquid HTML by cow push
      _shell.tsx                  shared module, not a template
    segments/
      loyal.json                  key: "loyal"
    scenarios/        JSON scenarios for `cow test`, named <automation>.<case>.json
```

An automation file holds the definition described in [Automations](/automations/), and its [JSON Schema](https://docs.cowliss.com/schemas/automation.json) is published, so map `automations/**/*.json` to it in your editor's JSON settings and it autocompletes every step. A segment file holds a [segment definition](/segments/): `match`, `predicates`, and optional `tags` and `description`.

- **The file name is the key.** Kebab-case (`^[a-z0-9]+(-[a-z0-9]+)*$`), unique within `automations/`, within `emails/`, and within `segments/`. An automation and a template may share a key, and usually do: the automation that sends `abandoned-checkout` is called `abandoned-checkout`. Both are unique per app, not per organization (see [Several apps, one organization](#several-apps-one-organization)), so two of your apps may each have a `welcome`, and every read of one names the app.
- **Directories are yours to organise.** Discovery walks the whole tree and takes the key from the file name, so `automations/commerce/recovery/winback.json` is just `winback`. How the dashboard groups an automation is its `tags`, which you write in the definition.
- **No file names an app.** The project is the app, so an automation's trigger, its `waitForEvent` steps, the segment it owns, and everyone it enrolls are all scoped to `cow.json`'s `appId` for you. There is nothing per file to get wrong.
- **A leading underscore means "shared module".** `emails/_shell.tsx` is skipped by discovery, so it is not a template. It stays in the repo and is imported by relative path (`import { Shell } from "./_shell"`), which is how several `.tsx` templates share one layout. A Liquid template is one file and includes nothing, so its layout is written into it.
- **Dot-prefixed files are ignored entirely.** A `.gitkeep` or a `.DS_Store` is tooling litter, not source, so nothing reads it.

A `.tsx` template is bundled and rendered on your machine when you push, once and with no props, into the Liquid HTML that is sent (see [Writing it as a React Email component](/emails/#writing-it-as-a-react-email-component)). It may import any package that bundles for the browser; an import that does not bundle stops the push with the file named. An automation is a definition, so it imports nothing.

## `cow.json`

```json title=".cowliss/cow.json"
{
  "$schema": "https://docs.cowliss.com/schemas/cow.json",
  "orgId": "org_2abcDEF",
  "appId": "app_shop"
}
```

That is the whole file, and it is deliberately boring. The org id is not a secret, and it is a guard: `cow push` refuses when the credential belongs to a different organization, so a wrong key or a wrong checkout stops before anything is written. `appId` says which of the organization's apps this repo ships for, and a push naming an app the organization does not have is refused with a **422**. Credentials never live here: they are in `~/.cow/credentials.json` after `cow login`, one session per API URL, or in `COW_PIPELINE_KEY`.

`apiUrl` and `webUrl` are the optional keys, and together they name the Cowliss this repo belongs to: the API it pushes to, and the dashboard `cow login` opens. The CLI takes the API from `--api`, then `COW_API_URL`, then `apiUrl`, then the hosted API; the dashboard from `--web`, then `COW_WEB_URL`, then `webUrl`. They travel together on purpose, because a token minted at one Cowliss's dashboard is rejected by another's API. Leave both out on the hosted product.

`cow login` keeps one login per API URL: signing in to a second Cowliss leaves your login for the first one in place. Every command uses the login for the API it talks to, so against an API you never signed in to it is not logged in. `cow whoami` and `cow logout` act on that API's login only.

One login can cover several organizations: tick every one the terminal should act in when the browser asks, and each gets its own token. A command run inside a repository acts in the organization its `cow.json` names; outside one, pass `--org <id>` or set `COW_ORG_ID`. With several organizations and none named, the command stops and lists them instead of guessing. `cow whoami` shows the organization commands here would use and every one the login covers.

`purposes` is optional too: the purposes this repo's emails are sent under, besides bare `transactional`. Each has a `key` that is a path, a root and a camelCase name, a short `label` the dashboard renders beside the switch, and an optional one-sentence `description` shown under it.

```json title=".cowliss/cow.json"
{
  "orgId": "org_2abcDEF",
  "appId": "app_shop",
  "purposes": [
    { "key": "marketing.newsletter", "label": "Newsletter" },
    {
      "key": "transactional.weeklyReport",
      "label": "Weekly report",
      "description": "Your store's numbers, every Monday."
    }
  ]
}
```

The root decides what an unanswered purpose means, so there is no default to write. A name under `marketing` is off until your app records a yes, because it is unanswered on every profile you already have. A name under `transactional` is on until the recipient says no: mail about their own account that they may still want to stop. The roots themselves are fixed, every app has them, and nobody declares them. Bare `transactional`, a receipt or a password reset, is not a purpose anyone can withhold, so it needs no declaration either.

Each [template](/emails/) names its purpose, and `cow plan` and `cow push` check it against this list: a template sent under a path this file does not declare, or under bare `marketing`, stops the push with the file named. A send under a name needs the name and its root, so turning `marketing` off silences every marketing name at once. An [automation](/automations/api/)'s `purpose` is only `marketing` or `transactional`, which decides how it enrolls.

Purposes are answered on a profile, and a profile belongs to one app, so the purposes belong to the app too: two apps may each declare the same path under their own label, and each app's profiles answer only their own app's. A push whose `cow.json` has `purposes` adds and updates the ones listed; one without the key leaves the app's purposes as they are. A push never removes a purpose, because your profiles hold answers against it: deleting one is its own verb, in the app's settings or over the API, and is refused while a template, an automation, a broadcast or a signup form still names it.

### `events`

`events` is optional: the events this app sends, as names or patterns. A pattern matches the way a [trigger](/automations/api/) does, with `*` standing for any run of characters, dots included.

```json title=".cowliss/cow.json"
{
  "orgId": "org_2abcDEF",
  "appId": "app_shop",
  "events": ["user.*", "order.paid", "order.refunded"]
}
```

What it does: every event name the app receives is tagged as listed or as not in `cow.json`, so a typo or a call nobody planned shows up next to the names you meant. A listed name that stops arriving is the other half of the check.

What it does not do: it accepts or refuses nothing. An event the list does not name is still stored, still counts toward segments, and still starts automations, exactly like a listed one. To list a new name, add it here and push, or add it on the app's **Events** tab in the dashboard.

The list belongs to the app. A `cow.json` with `events` replaces the app's list whole, so an entry you delete here is gone after the next push, and `"events": []` empties it, so every name reads as not listed. A `cow.json` without `events` leaves the app's list as it is, which is how a team that edits the list in the dashboard keeps it there. Turning the check off altogether is done in the dashboard. `cow plan` and `cow push` show how a push moves the list, and a push that would change a list somebody edited in the dashboard is a [conflict](#conflicts). An empty entry, one longer than 200 characters, or one under `system.` (events Cowliss records itself) stops `cow plan` and `cow push` with the entry named.

To point the same checkout at a different Cowliss (your own local one, say), keep a second config beside this one and select it with `--config`:

```sh
cow push --config cow.local.json
```

The [JSON Schema](/schemas/cow.json) is generated from the same definition the CLI validates with, so an editor autocompletes it.

## What a push changes

A push adds and updates every automation, template and segment the tree names and touches nothing it does not name: `events` present replaces the app's list and absent leaves it, `purposes` present adds and updates the ones listed and absent leaves them, and deleting anything is always an explicit verb.

So a file you deleted from the repo leaves its key where it is on the server (see [Deleting, and undoing](#deleting-and-undoing)), and a partial checkout cannot take out an automation it does not carry.

## Checks before anything is sent

`cow plan` and `cow push` read the whole tree on your machine before they call the API, and stop at the first problem with the file named:

- every automation and segment file against its schema (a code automation, `automations/*.ts`, is refused with a pointer to the definition format), and every trigger that names a segment file against the files that exist;
- every template's front matter, every `.tsx` rendered into its Liquid HTML, and every Liquid template parsed and rendered once with no props, so a tag left open or a filter that does not exist stops here;
- every template's purpose against the ones `cow.json` declares, and `cow.json` itself.

Nothing is written to a `build/` directory and nothing is generated into the tree. Limits: 100 automations, 200 templates and 100 segments per push.

`cow test` runs the scenarios in `scenarios/` against the definitions, with no network (see [Testing automations](/automations/testing/)). A project with automations and no scenarios fails it. A project with no automations, only templates, has nothing to run, so `cow test` says so and passes, and `cow test && cow push` works for either.

## Push, and what a version is

```sh
cow push
```

`push` reads the tree, then shows you what it would change before it changes anything:

```text
Plan for app_shop:

+ automation  abandoned-checkout
~ automation  winback             from, steps wait remind
  template  receipt
~ events    +order.refunded

1 to add, 1 to change, 1 unchanged.

Left alone:
  automation  legacy              on the server, not in this tree
  automation  winback             on hold; this leaves it that way
Push this? [y/N]
```

A `+` is a key your app does not have yet, a `~` is one this tree moves, and the words beside it are the fields that moved: for an automation, `steps` followed by the ids of the steps added or changed. The `events` and `purposes` lines say how the push moves the app's event list and purposes. **Left alone** is drift: something your app holds that this push does not write. A key you deleted from the repo stays where it is; an automation somebody put on hold stays on hold. We report it and never fight it.

Answer `y` and it applies. `--yes` answers for you and is what a pipeline passes; a run with no terminal and no `--yes` refuses rather than assuming you agreed. Nothing is written until you have said yes.

The same diff on its own is `cow plan`: it reads, asks, prints and stops, with nothing to answer. It stores nothing, so it is what to run in review, in a pipeline before the push, or as the read an agent takes before it changes anything. `cow plan --json` prints the diff as one document.

Applying sends the resources inline and writes them in one transaction, filing a new **version** of every automation whose definition changed and every template whose content changed. A key nothing touched gets no new version. Every version is ready the moment it is filed, and the command prints where each changed key now lives:

```text
Pushed 2 keys:
  abandoned-checkout  https://app.cowliss.com/apps/app_shop/automations/abandoned-checkout
  winback             https://app.cowliss.com/apps/app_shop/automations/winback
```

A tree your app already holds plans `Nothing to change.` and stops there: it asks nothing and writes nothing. A tree where no key moved but the event list or a purpose did plans `No key to add or change.` and asks before it writes those.

A send renders the template's latest version, so a fixed typo reaches the next send.

**A push never turns anything on or off.** The switch belongs to the key, not to the file.

## Conflicts

A conflict is a resource this push would change whose current content was written in the dashboard, over the API or through MCP since the last push: a template somebody edited, a segment made by hand, the event list or a purpose changed on the app's page. The plan names every one first, with when it was written, before the rest of the diff:

```text
Edited in the dashboard or over the API since the last push, and this push would overwrite:
! template  winback-offer  edited 2026-10-24T14:02:11.000Z
Pull first (cow pull), or push with --force to overwrite.

Plan for app_shop:

~ template  winback-offer  html, subject

0 to add, 1 to change, 0 unchanged.
Overwrite those edits and push this? [y/N]
```

There are two ways out, and both are deliberate:

- **Keep the edit.** `cow pull` writes what the app holds into the tree, so the edit becomes a change in your repo you can review and commit. Push again and there is nothing to overwrite.
- **Overwrite it.** Answer `y` at the prompt, or run `cow push --force`. A `y` accepts exactly the conflicts the prompt listed, as they were when the plan read them: if one of them is edited again in the dashboard before the push lands, the push is refused and asks you to run `cow plan` again. `--force` accepts whatever is there.

`--yes` never confirms a conflict: on a terminal you are still asked, and without one the push refuses and writes nothing, naming each conflict. A pipeline that means to overwrite edits made in the dashboard says so with `--force` in its own command line.

Content that matches is no conflict, whoever wrote it: a push of the same template somebody made in the dashboard takes it over silently. Nothing about a conflict is recorded in the repo, so a committed `cow.json` never goes stale and a pipeline never has to commit anything back.

## Turning an automation on

```sh
cow enable abandoned-checkout
cow enable abandoned-checkout winback
cow disable --all
```

Every automation is off until you turn it on. Turning one on or off takes an admin, or a pipeline key, because it is the switch that lets an automation reach real people. `--all` reads this app's keys from the server, so it reaches a key whose file the tree has lost.

Disabling stops new executions and lets the ones in flight finish. To stop those too:

```sh
cow executions cancel --automation abandoned-checkout
cow executions cancel --version ver_01j…
```

Cancelling takes an admin, and each execution stops when it reaches its next step. An execution stays on the version it started on for its whole life, so a user halfway through a 30-day automation finishes the steps they began under, whatever you push over it. Templates are the other way round: a send resolves the latest version of the template at the moment it sends, so fixing a typo and pushing reaches executions that are already running.

An organization holds one set of data, so an automation you turn on runs against real people. To try the whole loop from your own app first, push to a second organization and point your dev box at its key: same commands, data nobody will miss. `cow test` and a dry run answer the smaller questions without leaving the organization you are in.

## Status

```sh
cow status
```

One line per key of this app: its latest version with the short digest, when it was written, whether the automation is on, and how many executions are running. Then the drift, in both directions: files you have changed since the last push, and keys the server holds that this tree no longer has, each with the command that removes it. It closes with the names an automation references that nothing defines yet, such as a webhook you have not created.

## Deleting, and undoing

A push adds and updates. On its own it never deletes: a partial checkout would otherwise take out production automations. Removing a key is explicit: either the verb below, or `cow push --prune` when you mean "the repository is the whole truth". `cow status` names the ones waiting for it:

```sh
cow automations delete winback
cow templates delete winback
```

Deleting an automation stops new entries and takes its switches with it, so a key you delete and push again comes back off. A template cannot be deleted while an automation still sends it; the refusal names the automations. Automation and template keys are unique per app, so inside a project every command reads the app from `cow.json`; outside one, say which is yours with `--app-id`: `cow status` prints the command with it.

### Pruning what the tree dropped

Once you are sure the repository is the whole truth, `cow push --prune` deletes the automations and segments your account holds and this tree does not, in the same run as the push:

```sh
cow push --prune
```

Before it deletes anything it prints one line per thing, and for an automation its state and **how many live executions cancelling it ends**, with the total. Read that number: it is people part-way through a sequence. Answer the prompt and it applies; decline and nothing at all happens.

Templates are never pruned. An execution is pinned to the template version it started on, so a template whose file went away keeps working for everyone still in flight; delete it with `cow templates delete` once nothing sends it.

A segment another automation still enrols from is refused rather than deleted, and the refusal names the automation. A segment made in the dashboard or over the API is never pruned: no push wrote it, so a tree that lacks it has not dropped it.

In a pipeline, `--yes` applies a prune without asking, except when the plan both adds and deletes, which is exactly what renaming a file looks like. That shape is refused with both keys named, because an unattended run is where nobody reads the live-run count. Confirm it yourself, or push the rename in two steps: push the new file, then prune once it is live. Pruning also needs an organization admin signed in with `cow login`: ending runs that are carrying real people is not something a pipeline key does, so a pipeline key's `--prune` is refused before anything is deleted.

To undo a push, put the files back and push again:

```sh
git revert <commit>
cow push
```

The push carries the older content, so the previous definition or template is what the next execution or send uses.

## Several apps, one organization

An organization can run several products, and each one is an app with its own repo. Run `cow init` in each, answer with that app's id, and each repo has its own `cow status` and its own `cow pull`. They share the organization, so they share webhooks, sending domains and the bill. Each repo's `cow.json` keeps its own [`events`](#events) list. They do not share people: a profile belongs to one app, so the same email address signing up on two of your products is two profiles, and an automation only ever sees its own app's events, profiles and segments.

Two rules keep the repos out of each other's way:

- **An automation key and a template key are unique per app.** Two apps may each push a `welcome` automation and a `welcome` template, and each app's is its own: every read of one names the app, which `cow.json` supplies inside a project and `--app-id` outside one.
- **A push only ever touches its own app's keys.** Nothing another repo pushed is removed, renamed, or switched off by yours.

One repo ships for one app, and one app is shipped by one repo. If you need a second product, create a second app and a second repo for it.

An automation sends only its own app's templates: a key resolves in the app that sends it, so a template another app pushed is never reached, whatever its name. The same goes for a send through an app's source. Two apps that want the same email share its source file and each push it.

## Pull

```sh
cow pull                       # the app cow.json names
cow pull --app-id app_shop     # into a directory with no cow.json yet
```

Writes what the app holds on the server into the tree as files, whoever wrote it: a push, the dashboard, an API call or MCP. That is the other way out of a [conflict](#conflicts), and the way to start a repo for an app somebody built in the dashboard.

- `cow.json`: the org, the app, its `purposes` and its `events` list. The local-only keys of a file already there (`$schema`, `apiUrl`, `webUrl`) are kept as they were.
- `automations/<key>.json`: each automation's definition.
- `segments/<key>.json`: each segment of the app that is not an automation's own.
- `emails/<key>.html` for a Liquid template, `emails/<key>.md` for a doc, or `emails/<key>.maily.json` for a doc the markdown cannot carry.

A key with a local `emails/<key>.tsx` is skipped and listed: the component is your source and the HTML its output, so the next push renders it again. Pull only writes files: it deletes nothing, and a file already at a path it writes is overwritten. So it refuses to run over a git tree with uncommitted changes, untracked files included, unless you pass `--force`. In a directory with no `cow.json`, `--app-id` names the app and the org comes from your session.

## Pushing from CI

Use a **pipeline key**, not a session. It reaches the push loop and nothing else, so a leaked pipeline credential cannot read your customer data:

```sh
cow settings pipelineKeys create --name "ci-deploy"
```

```yaml title=".github/workflows/automations.yml"
name: push automations
on:
  push:
    branches: [main]
jobs:
  push:
    runs-on: ubuntu-latest
    env:
      COW_PIPELINE_KEY: ${{ secrets.COW_PIPELINE_KEY }}
      COW_API_URL: https://api.cowliss.com
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 24 }
      - run: npm ci
      - run: npx cow test
      - run: npx cow push --yes
```

`--yes` applies the plan without a prompt, and still refuses a plan with a [conflict](#conflicts), so a pipeline never overwrites an edit made in the dashboard without saying so. Leave `--force` out of CI unless the team means "the repo always wins".

`COW_PIPELINE_KEY` wins over any cached login on the same machine, deliberately: a stale session left in `~/.cow` must never quietly become the identity a pipeline runs as. See [Keys](/guides/keys/) for what each credential can reach.

## Environment variables

| Variable | What it does |
| --- | --- |
| `COW_API_URL` | The API to talk to, and so which `cow login` session is used. `--api` beats it; it beats `apiUrl` in `cow.json`. |
| `COW_ORG_ID` | The organization to act in when the login covers several. `--org` beats it; it beats `orgId` in `cow.json`. |
| `COW_PIPELINE_KEY` | A pipeline key; takes precedence over `COW_TOKEN` and a cached session. |
| `COW_TOKEN` | A session token passed explicitly, instead of a cached `cow login`. |
| `COW_CREDENTIALS_PATH` | Where the cached logins live, one per API URL with a token per organization (default `~/.cow/credentials.json`). |

## Where next

- [Automations](/automations/): the definition, its steps and rules.
- [Emails](/emails/): Liquid and doc templates.
- [Testing automations](/automations/testing/): scenarios, dry runs, and a second organization.
- [Blueprint gallery](/blueprints/): three annotated automations you can copy in.
