> ## Documentation Index
> Fetch the complete documentation index at: https://docs.autosana.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Code-Managed Flows

> Define flows, suites, and hooks as files in your repository, synced via GitHub

Keep your flows, suites, and hooks in version control by defining them as files in your repository's `.autosana/` folder. When you push to your default branch, Autosana syncs those files and materializes them as flows and suites in your dashboard. This is GitOps for your tests: your repository is the source of truth, changes go through pull requests, and every definition is code-reviewed alongside the app it tests.

Code-managed definitions are **read-only in the dashboard** — you edit them by changing the YAML and pushing (or opening a PR to preview first). Running is unaffected: read-only covers the definition, not execution.

<Note>
  **Beta.** Code-managed flows are an early feature. The flow- and suite-YAML validation rules are shared by the sync and the `autosana` CLI, so those parts validate identically on your machine. Checks that need your organization's data — app names, hook references, and hook-file contents — run only server-side; see [Validating locally](#validating-locally).
</Note>

## How it works

1. You define flows, suites, and hooks as files under `.autosana/` in your repo
2. On a push to your default branch, Autosana fetches `.autosana/`, validates every file, and materializes the changes
3. On a pull request, Autosana previews the proposed flow definitions and posts a GitHub check
4. The dashboard shows the resulting flows and suites as **read-only** — edit them by pushing to the repo

## Enabling code-managed flows

Code-managed flows are powered by the [GitHub Bot](/github-integration), so install the GitHub App first if you haven't.

1. Go to **[Settings > Integrations](https://autosana.ai/settings?tab=integrations)**
2. Find the repository you want to manage in the connected-repositories list
3. Toggle **Code-Managed Testing** on for that repo
4. Autosana runs an initial full sync of `.autosana/` immediately

The toggle is per repository, so you can opt in one repo at a time. Enabling it kicks off an initial sync; after that, every push to the default branch that touches `.autosana/` re-syncs automatically, and a nightly resync catches any drift.

<Note>
  You need to be an admin of your Autosana organization to install the GitHub App and manage integration settings.
</Note>

## Migrating existing flows

Already have flows in the dashboard? Export them to files with the `autosana` CLI, commit them, and let the sync adopt them:

```bash theme={null}
# Write every dashboard flow, suite, and their hooks to ./.autosana (with explicit keys)
autosana flows export --all

# Or export a single suite and its flows
autosana flows export --suite "Smoke Tests"
```

Export writes each flow (`*.flow.yaml`), suite (`_suite.yaml`), and the flows' setup/teardown **hooks** (`hooks/<slug>.<ext>`), and adds `setup_hooks`/`teardown_hooks` to the flow files. cURL hooks have no file form, so they're referenced by slug but stay dashboard-authored. Review the generated files, commit them under `.autosana/`, then enable **Code-Managed Testing**.

On the first sync, anything whose identity **matches** an active dashboard definition is **adopted** — its run history is preserved and it becomes code-managed (read-only), now driven by your files:

* a **flow** or **suite** is adopted when its `name` exactly matches one active dashboard flow/suite;
* a **hook** is adopted when its slug (its filename) matches one active dashboard hook.

<Warning>
  Adoption is by **exact, case-sensitive** identity and only on a single match. A YAML flow named the same as an *unrelated* dashboard flow — or a hook file whose slug matches an unrelated dashboard hook — will claim it and make it read-only, so review your exported names before enabling. If two dashboard flows share a name (ambiguous), the sync creates a new flow instead of claiming either. A hook slug that collides with a **different repository's** hook is still a blocking error (see [Hooks as files](#hooks-as-files)).
</Warning>

## Repository layout

Everything Autosana manages lives under a single `.autosana/` folder — at your repository root by default, or under a subdirectory you point Autosana at (see [Monorepos](#monorepos)). Flows are `*.flow.yaml` files, a suite is a folder containing a `_suite.yaml` manifest, and hooks are script files under a `hooks/` directory.

```text theme={null}
.autosana/
├── login.flow.yaml          # a root flow (folder "")
├── hooks/
│   └── seed-db.py           # a hook — slug "seed-db"
└── smoke/                   # a suite folder
    ├── _suite.yaml          # the suite manifest (suite key "smoke")
    ├── checkout.flow.yaml   # a flow in the smoke suite
    └── hooks/
        └── reset-test-env.sh  # another hook — slug "reset-test-env"
```

| File type | Location                                                    | Purpose                         |
| --------- | ----------------------------------------------------------- | ------------------------------- |
| **Flow**  | `<name>.flow.yaml` (anywhere under `.autosana/`)            | One test flow                   |
| **Suite** | `<folder>/_suite.yaml` (one per folder)                     | Groups the flows in that folder |
| **Hook**  | `<any>/hooks/<name>.<ext>` (direct parent must be `hooks/`) | A setup/teardown script         |

<Note>
  A file is only recognized if it ends in `.flow.yaml`, is named exactly `_suite.yaml`, or sits directly inside a `hooks/` folder with a supported extension. Other files under `.autosana/` are ignored.
</Note>

## Flow files

A flow file ends in `.flow.yaml`. The only required keys are `name` and `instructions`, where `instructions` is either a single multiline prompt or a YAML list of steps.

As a multiline prompt:

```yaml theme={null}
key: login
name: Login
instructions: |
  Log in as qa+smoke@example.com using the password in ${env:LOGIN_PASSWORD}.
  Open the account menu and verify the signed-in email is shown.
  Sign out and confirm you land back on the login screen.
```

Or as a list of steps:

```yaml theme={null}
key: checkout
name: Checkout happy path
description: Buys one item as a logged-in user
caching: false
instructions:
  - From the home screen, tap the cart icon
  - Tap "Checkout"
  - Verify the order confirmation screen appears
```

The recognized top-level keys are:

| Key                 | Required  | Notes                                                                                                                                                                           |
| ------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **name**            | Yes       | Non-empty string.                                                                                                                                                               |
| **instructions**    | Yes       | A YAML list of non-empty step strings, or a single non-empty prompt string. A list is numbered into a single prompt.                                                            |
| **key**             | No        | Unique flow key. Defaults to the path minus `.autosana/` and `.flow.yaml` (e.g. `smoke/checkout`).                                                                              |
| **description**     | No        | Free-text description.                                                                                                                                                          |
| **app**             | Sometimes | Name of a linked app. Optional when the repo is linked to 0 or 1 apps; **required on every flow once the repo is linked to more than one app**. An unknown name fails the sync. |
| **caching**         | No        | Boolean, defaults to `true`. Only has an effect when [Run Caching](/flows#run-caching) is enabled for your org.                                                                 |
| **setup\_hooks**    | No        | Ordered list of hook slugs to run before the flow.                                                                                                                              |
| **teardown\_hooks** | No        | Ordered list of hook slugs to run after the flow.                                                                                                                               |

<Warning>
  Any top-level key that isn't in the table above is a hard error. A typo like `instructoins:` fails validation with a "Did you mean 'instructions'?" suggestion on the [Autosana Flows check](#sync-status--validation), so mechanical mistakes are caught before they merge.
</Warning>

<Tip>
  **Let the path derive your key.** If you omit `key`, the key comes from the file path (`.autosana/smoke/checkout.flow.yaml` becomes `smoke/checkout`). Set an explicit `key` when you want the key to stay stable even if you move or rename the file — see [Deleting and renaming](#deleting-and-renaming).
</Tip>

### Referencing variables

Reference environment variables inside `instructions` with the `${env:VAR_NAME}` token. This is **reference only** — the values stay defined in the dashboard and are resolved just in time when the flow runs. There is no `env:` block in the YAML; the value never lives in your repo.

```yaml theme={null}
name: Login
instructions: |
  Log in with email ${env:TEST_EMAIL} and password ${env:TEST_PASSWORD}.
  Verify you land on the welcome screen.
```

Define and manage the actual values in the dashboard — see [Variables](/variables) for the full variable model and [Environments](/environments) for per-environment values.

### Referencing hooks

Attach hooks to a flow with `setup_hooks` and `teardown_hooks`, each an ordered list of hook **slugs**:

```yaml theme={null}
name: Checkout happy path
setup_hooks:
  - seed-db
teardown_hooks:
  - reset-test-env
instructions:
  - Add an item to the cart and check out
  - Verify the order confirmation appears
```

A slug resolves against **every active hook in your organization** — not just repo files — so you can also reference hooks authored in the dashboard, including cURL hooks (which can only be created in the dashboard). Find a dashboard hook's slug on the hook in the [Hooks](/hooks) page. See [Hooks as files](#hooks-as-files) for how a repo file's slug is derived.

Duplicate slugs within one list are rejected. The same slug may appear in both `setup_hooks` and `teardown_hooks` — that's legal. To reference a hook by name inside your instructions at run time, use `${hooks:Hook Name}` (by display name); see [Hooks](/hooks).

## Suites

A suite is a folder containing a `_suite.yaml` manifest. The suite's key is its folder path (a `smoke/` folder gives the suite key `smoke`). The only required key is `name`:

```yaml theme={null}
name: Smoke Tests
description: Critical-path checks
setup_flow: login
instructions: |
  Test account is qa+smoke@example.com.
flows: [login, checkout]
```

The recognized keys are:

| Key                                    | Required | Notes                                                                                                                                                                    |
| -------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **name**                               | Yes      | Non-empty string.                                                                                                                                                        |
| **flows**                              | No       | Ordered list of flow-key references. Omit it to include every flow **directly in the suite's own folder** (subfolders are not included), in filename-alphabetical order. |
| **setup\_flow**                        | No       | A single flow-key reference to run first.                                                                                                                                |
| **description**                        | No       | Free-text description.                                                                                                                                                   |
| **instructions**                       | No       | Suite-level context shared across the suite's flows (e.g. shared test-account info).                                                                                     |
| **setup\_hooks** / **teardown\_hooks** | No       | Ordered lists of hook slugs.                                                                                                                                             |

References in `flows` and `setup_flow` resolve **sibling-first**: a flow directly in the suite's own folder wins, otherwise the reference is matched against every flow in the repo. Flows in a subfolder are not default members — list them explicitly (or give the subfolder its own `_suite.yaml`). Flow keys and suite keys must each be unique across the whole repo. An unknown reference, or the same flow listed twice, is a validation error.

<Note>
  The `_suite.yaml` file must live inside a folder — it cannot sit at the `.autosana/` root, because a suite *is* a folder. Membership is defined by the suite (its `flows` list or its folder), never declared inside the flow files themselves.
</Note>

<Warning>
  YAML parses bare `no`, `yes`, `on`, and `off` as booleans (the "Norway problem"). Because `setup_flow` and `name` must be strings, `setup_flow: no` fails validation — quote the value if you really mean the string `"no"`.
</Warning>

For how variables and setup flows propagate across a suite's flows, see [Suites](/suites).

## Hooks as files

Hooks live as script files inside a `hooks/` folder — either `.autosana/hooks/` or `<suite>/hooks/`. The file extension determines the hook type, and the filename becomes the hook's slug.

| Extension | Hook type                        |
| --------- | -------------------------------- |
| `.py`     | Python script                    |
| `.js`     | JavaScript script                |
| `.ts`     | TypeScript script                |
| `.sh`     | Bash script                      |
| `.json`   | Launch args (must be valid JSON) |

The **slug** is derived from the filename with its final extension removed: lowercased, with runs of non-alphanumeric characters collapsed to `-`. So `Seed DB.sh` becomes slug `seed-db`, and `reset.env.json` becomes `reset-env`. This slug is what you reference in `setup_hooks` / `teardown_hooks`. The **display name** is the humanized filename (`reset-test-env.sh` shows as "Reset Test Env").

<Warning>
  **Hook slugs are unique across your whole organization** — the folder a hook lives in has no scoping effect. Two files that slugify the same (`seed-db.py` and `seed_db.sh`), or a slug that collides with a **different repository's** hook, fail the sync with a rename error. A slug that matches an existing **dashboard-authored** hook is *not* an error — that hook is adopted (its run history preserved), the same way flows adopt by name; see [Migrating existing flows](#migrating-existing-flows). Rename the file if that isn't what you intend.
</Warning>

<Note>
  The file content **is** the hook script, stored verbatim. Empty or whitespace-only files are rejected. A `.json` launch-args hook must parse as valid JSON, and a filename with no letters or digits (so it produces an empty slug) is rejected.
</Note>

Script hooks (`.py`, `.js`, `.ts`, `.sh`) read environment variables **natively** — `os.environ.get(...)` in Python, `process.env` in JavaScript/TypeScript, `$VAR` in Bash. Launch-args (`.json`) hooks — and dashboard-authored cURL hooks — use the `${env:KEY}` token instead, since they aren't executed scripts. cURL hooks have no file form; reference an existing dashboard cURL hook by slug, or use a `.sh` hook that runs `curl`. See [Hooks](/hooks) for the full hook model.

## Monorepos

By default Autosana looks for `.autosana/` at your repository root. If your definitions live in a subdirectory — common in a monorepo — point Autosana at that folder with the repo's **Root directory** setting.

1. Go to **[Settings > Integrations](https://autosana.ai/settings?tab=integrations)**
2. On the repo with **Code-Managed Testing** enabled, set **Root directory** to the folder that contains `.autosana/` (for example, `services/mobile` for `services/mobile/.autosana/`)
3. Leave it blank to use the repository root

Flow and suite **keys stay relative to `.autosana/`**, so moving your definitions into a subdirectory doesn't change any keys — a flow at `services/mobile/.autosana/login.flow.yaml` still has the key `login`. The GitHub links on each row point at the full path in your repo.

<Note>
  Saving a new Root directory doesn't re-sync on its own, and an unrelated push won't either. To re-sync from the new location, push a change under `<root-directory>/.autosana/` on your default branch, or toggle **Code-Managed Testing** off and on. The nightly resync also picks it up within a day.
</Note>

## Validating locally

Validate your `.autosana/` folder before you push using the `autosana` CLI. Install it (requires **0.8.0 or newer**, which adds the `flows` commands):

```bash theme={null}
pip install "autosana>=0.8.0"   # or: uvx autosana flows validate
```

Then validate — by default it checks the `.autosana` directory in the current folder; pass a path to validate somewhere else (handy for a monorepo):

```bash theme={null}
autosana flows validate
autosana flows validate services/mobile/.autosana
```

The CLI runs the **flow- and suite-YAML** rules exactly as the sync does — required keys, unknown keys, `instructions` shape, suite references, key uniqueness — and reports each error per file with the offending line. It exits non-zero on errors, so you can wire it into a pre-commit hook or CI step.

<Note>
  The CLI can't see your organization's data, so a few checks run **only** server-side and appear on the Autosana Flows check after you push: `app:` name resolution, hook-slug references and conflicts, and hook-file contents (empty scripts, invalid launch-args JSON). A green local run doesn't guarantee a green check if a flow references an unknown app or hook.
</Note>

## Sync status & validation

Every sync posts a GitHub check named **Autosana Flows** on the head commit — on both pushes and pull requests. It concludes as a failure if there are any issues, otherwise success. The summary reports how many files synced (or lists the errors found), and parse errors are attached as inline annotations anchored to the offending line where one is known, with any "Did you mean" hint appended.

<Warning>
  **One broken file freezes the whole repo's materialization.** The apply step runs only when the entire `.autosana/` folder validates with zero issues. If any file has an error, the check reports everything, but no changes are applied and previously synced flows stay as they were until you fix it. The check makes this loud.
</Warning>

## Pull request workflow

Open a pull request that changes `.autosana/` and Autosana previews your flow changes without touching mainline:

* Each changed or added `*.flow.yaml` gets a **PR-scoped flow version** pinned to the PR, so the PR's test runs use the proposed definitions
* The **Autosana Flows** check runs on the PR. For mechanical typos on lines you changed, Autosana also posts inline GitHub *suggestion* comments you can apply in one click
* Suite manifest (`_suite.yaml`) changes are **preview-only** on a PR — a warning notes that suite changes apply when the PR merges
* Removed flow files are ignored during preview; the archival happens on merge
* **Hook files are not previewed.** A PR's runs use the mainline hook scripts, and hook changes (plus their slug-conflict validation) apply on merge. A PR touching *only* hook files posts no check at all

<Warning>
  **The PR check validates each changed flow file on its own.** Cross-file checks — repo-wide key uniqueness, suite references, hook slugs and conflicts, and `app:` names — run at merge on the mainline sync, not on the PR. A PR can go green and still fail after merge (for example, a new file whose key duplicates an existing flow). Run `autosana flows validate` before merging to catch cross-file issues early.
</Warning>

<Note>
  **Pull requests from forks are skipped** — no flow versions are minted and no check is posted. Fork-authored YAML never touches your organization; it's validated and applied by the mainline sync only after the PR merges.
</Note>

When the PR merges, the merge lands as a push to your default branch, and the mainline sync materializes everything for real: new flows go live, PR-created flows are promoted to mainline, and suite, hook, and deletion changes apply.

## Deleting and renaming

Removing a definition is a soft delete. When you delete a `.flow.yaml`, `_suite.yaml`, or hook file and push to your default branch, Autosana **archives** the corresponding flow, suite, or hook — it's deactivated and hidden, but its run history is kept. If a file with the **same key** returns later, the exact same record is reactivated (history intact).

<Warning>
  **Renaming or moving a flow file without an explicit `key` splits its history.** Because the key defaults to the file path, moving `login.flow.yaml` to `auth/login.flow.yaml` archives the old flow (`login`) and creates a new one (`auth/login`). Set an explicit `key:` (see the [Tip above](#flow-files)) so the record — and its run history — follows the file.
</Warning>

Deleting a **hook file** while any flow or suite still references its slug fails the whole sync with an "unknown hook" error. Remove the `setup_hooks` / `teardown_hooks` references in the same commit as the hook file.

## Read-only in the dashboard

A flow, suite, or hook is code-managed when it originates from a connected repository. Code-managed rows are **read-only** in the dashboard:

<Note>
  To change a code-managed flow, suite, or hook, edit the YAML (or script) in your repository and push — or open a PR to preview the change first. The dashboard links each code-managed row to its file on GitHub. Attempting to edit a code-managed definition in the dashboard is blocked; the repository stays the source of truth.
</Note>

Read-only applies to the **definition**, not execution — you can still run code-managed flows and suites from the dashboard. Environment variable **values** also stay defined in the dashboard; your YAML only references them via `${env:KEY}`.

<Warning>
  **Disabling the toggle does not restore dashboard editing.** Turning **Code-Managed Testing** off only stops syncing; already-synced flows, suites, and hooks stay code-managed and read-only. To make them editable in the dashboard again, first delete their files and push (which archives them), then disable the toggle. As long as the toggle is on, your YAML stays authoritative.
</Warning>

## Troubleshooting

### My changes didn't appear in the dashboard

**Possible Causes:**

* The **Code-Managed Testing** toggle isn't enabled for the repository
* The push wasn't to the default branch, or didn't touch any file under `.autosana/` (or under `<root-directory>/.autosana/`)
* Your `.autosana/` folder isn't at the repository root and no **Root directory** is set for the repo
* Another file in `.autosana/` has a validation error, so the whole apply step was skipped
* After a **force-push** that rewinds the branch to older-dated commits, the sync may be skipped as stale — push a newer commit, or toggle Code-Managed Testing off and on to force a full resync

**Solutions:**

* Enable the toggle in **[Settings > Integrations](https://autosana.ai/settings?tab=integrations)**
* Confirm you pushed to the default branch and edited a file under `.autosana/`
* Open the **Autosana Flows** check on the commit and fix every reported error — apply only runs at zero issues

### The Autosana Flows check failed

**Possible Causes:**

* An unknown top-level key or a malformed `instructions` value
* A suite reference that doesn't match any flow key, or a duplicate flow/suite key
* A `setup_hooks` / `teardown_hooks` slug that doesn't match any hook, or two hook files whose slugs collide
* An `app:` name that doesn't match a linked app — or an omitted `app:` when the repo is linked to more than one app
* A hook file that's empty, or a `.json` launch-args hook that isn't valid JSON

**Solutions:**

* Read the inline annotations and any "Did you mean" suggestions on the check
* Run `autosana flows validate` locally to reproduce the flow/suite YAML errors before pushing (app and hook errors surface only on the check)

### My PR check passed but nothing applied after merge

**Possible Causes:**

* The PR check validates each changed flow file on its own; cross-file checks (duplicate keys, suite references, hook slugs/conflicts, `app:` names) run at merge

**Solutions:**

* Open the **Autosana Flows** check on the merge commit and fix the reported cross-file errors
* Run `autosana flows validate` on the whole `.autosana/` folder before merging

### The dashboard won't let me edit a flow

**Possible Causes:**

* The flow is code-managed and therefore read-only in the dashboard

**Solutions:**

* Edit the linked `.autosana/` YAML file in your repository and push, or open a PR to preview the change

## Next Steps

* [Write effective flows →](/flows)
* [Organize flows with Suites →](/suites)
* [Learn about hooks →](/hooks)
* [Manage variables →](/variables)
* [Set up the GitHub Bot →](/github-integration)
