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

# GitOps

> Sync a workspace's flows, agents, knowledge bases and other config to a Git repository — treat your Noxus configuration as code.

<Note>
  **Beta — enabled per deployment.** Reach out to an administrator to turn GitOps on
  for your tenant. Behaviour and the on-disk format may still change.
</Note>

GitOps serialises a workspace's **configuration** to YAML in a Git repository and
keeps the database and the repo in sync — in either direction. It lets you review
config changes as pull requests, promote a workspace between environments, and keep
an auditable, versioned backup of everything you've built.

Only **config entities** sync. Runtime data — runs, conversation history, ingested
documents — is never written to Git.

## What syncs

Each Noxus entity is exported as one **artifact** (the `noxus/v4` versioned
representation). GitOps operates over these artifact kinds, one directory per kind:

| Kind             | Repo directory     |
| ---------------- | ------------------ |
| `flow`           | `flows/`           |
| `flow_version`   | `flows/versions/`  |
| `agent`          | `agents/`          |
| `agent_version`  | `agents/versions/` |
| `knowledge_base` | `knowledge-bases/` |
| `inbox`          | `inboxes/`         |
| `file`           | `files/`           |
| `connection`     | `connections/`     |
| `trigger`        | `triggers/`        |
| `deployment`     | `deployments/`     |
| `secret`         | `secrets/`         |

By default **every kind** is in scope. The per-kind toggle in the UI opts a kind
*out*; you can also narrow scope with `includeSlugs` / `excludeSlugs`.

<Note>
  **Secrets sync, their values don't.** For a `secret` — and the credential on a
  `connection` — only the entry travels to Git: its name, id, and how other artifacts
  reference it. The encrypted value never leaves the database.
</Note>

## The artifact format

Every artifact is a single YAML document — an *envelope* with a stable header and a
kind-specific `spec`:

```yaml theme={null}
apiVersion: noxus/v4
kind: flow
schemaVersion: 1
metadata:
  slug: invoice-extractor-a1b2c3
  name: Invoice Extractor
  id: 0b3d9e7c-1f42-4a8e-9c11-a1b2c3d4e5f6
  description: Pulls totals and line items out of PDF invoices
  labels: {}
spec:
  # kind-specific configuration (nodes, edges, prompts, settings…)
```

* **`metadata.id`** is the artifact's identity. The trailing hex on the `slug`
  (`…-a1b2c3`) is derived from that id, so renaming an entity never rebinds it to a
  different one.
* **References** between artifacts (a trigger pointing at a flow, an agent using a
  knowledge base) are stored as ids inside `spec`, so the whole set stays internally
  consistent when it moves.

### Repository layout

```
<repo>/
├── .noxus/
│   └── manifest.yaml          # export metadata
├── flows/
│   ├── invoice-extractor-a1b2c3.yaml
│   └── versions/
│       └── invoice-extractor-a1b2c3.yaml
├── agents/
│   └── support-bot-9f0e1d.yaml
├── knowledge-bases/
├── connections/
├── triggers/
├── deployments/
└── secrets/
```

## Enable and connect

<Steps>
  <Step title="Set up the GitHub provider (one-time, admin)">
    Before anyone can connect an account, an administrator registers an OAuth app so Noxus can
    speak to GitHub on a user's behalf. On GitHub, go to **Settings → Developer settings →
    OAuth Apps → New OAuth App**, set the **Authorization callback URL** to
    `https://<your-noxus-domain>/api/backend/integrations/oauth/callback`, and copy the
    generated **Client ID** and **Client Secret**. Enter both in the Noxus Admin panel for the
    GitHub provider, granting the `repo` scope — read **and** write, since GitOps pushes
    commits. See [Admin: connecting integration providers](/integrations/admin-setup) for the
    full walkthrough.

    If your deployment ships the Noxus-managed GitHub provider, you can skip this — no client
    ID or secret needed. GitLab is configured the same way; generic Git uses an access token
    (or username + password) instead of OAuth.
  </Step>

  <Step title="Connect a Git account">
    Once the provider exists, each user connects their own account in **Settings →
    Integrations** (**GitHub**, **GitLab**, or **generic Git**). This credential is what GitOps
    uses to read and write the repo.
  </Step>

  <Step title="Point at a repository">
    Open **Settings → GitOps**, pick the connection, then set the repository
    (`owner/repo`) and branch (defaults to `main`).
  </Step>

  <Step title="Choose what syncs and how">
    Select the artifact kinds, pick a **direction**, and set the **sync interval**.
    Save the configuration.
  </Step>

  <Step title="Run it">
    Click **Sync now** for an immediate run, or let the scheduler pick it up. Use
    **Test** first to confirm the connection can read and write the repo.
  </Step>
</Steps>

## Direction and scheduling

GitOps runs in one of three directions:

| Direction               | Behaviour                                                    |
| ----------------------- | ------------------------------------------------------------ |
| `db_to_git` *(default)* | Push workspace config out to the repo.                       |
| `git_to_db`             | Import config from the repo into the workspace.              |
| `bidirectional`         | Sync both ways; `authoritative` (`db` or `git`) breaks ties. |

Scheduling is controlled by `syncIntervalMinutes` (default `15`). A background worker
runs every five minutes and syncs each **enabled** workspace whose interval has
elapsed. Set the interval to `0` for **manual-only** — the scheduler skips the
workspace, but **Sync now** still works.

<Info>
  The `enabled` flag only gates the *scheduler*. A manual **Sync now** needs just a
  valid connection and repository. A bidirectional manual run returns two run records
  (one per direction); single-direction returns one.
</Info>

## Per-artifact push and pull

You don't have to sync the whole workspace. From the editor, the Git control on a
flow or agent shows whether that artifact is linked, ahead, or behind the repo, and
lets you **push** or **pull** just that one — optionally **with its dependencies**
(e.g. pushing a flow can carry the knowledge bases and connections it references).

## The API

All endpoints are scoped to a workspace (`group_id`). Mutating calls require the
workspace **`integrations:edit`** permission; reads require membership. Every route
returns `404` while `GITOPS_ENABLED` is off.

| Method | Path                                                     | Purpose                                  |
| ------ | -------------------------------------------------------- | ---------------------------------------- |
| `GET`  | `/groups/{group_id}/git-sync/config`                     | Read the sync config                     |
| `PUT`  | `/groups/{group_id}/git-sync/config`                     | Update the sync config                   |
| `POST` | `/groups/{group_id}/git-sync/test`                       | Test the connection can read/write       |
| `GET`  | `/groups/{group_id}/git-sync/health`                     | Connection + repo access status          |
| `GET`  | `/groups/{group_id}/git-sync/repos`                      | List repos the connection can see        |
| `POST` | `/groups/{group_id}/git-sync/dry-run`                    | Preview the changes a sync would make    |
| `POST` | `/groups/{group_id}/git-sync/run`                        | Trigger a full sync now                  |
| `GET`  | `/groups/{group_id}/git-sync/runs`                       | Last 50 sync runs                        |
| `GET`  | `/groups/{group_id}/git-sync/runs/{run_id}`              | One run with its per-artifact changes    |
| `GET`  | `/groups/{group_id}/git-sync/artifacts/{kind}/{id}`      | Sync status of one artifact              |
| `POST` | `/groups/{group_id}/git-sync/artifacts/{kind}/{id}/push` | Push one artifact (`?with_dependencies`) |
| `POST` | `/groups/{group_id}/git-sync/artifacts/{kind}/{id}/pull` | Pull one artifact (`?with_dependencies`) |

<Warning>
  This is the internal management API for the settings UI, not the public v1 API — it
  is excluded from the production OpenAPI schema and its shape can change while GitOps
  is in beta. Don't hard-code against it in production integrations yet.
</Warning>

## Health and run history

`GET …/health` reports one of `healthy`, `connection_missing`, `connection_invalid`,
or `repo_unreachable`, along with whether the repo `exists` and is `can_read` /
`can_write`. If the connection breaks, sync degrades to local-only rather than
erroring, and the failure is recorded as a categorised run instead of a 500.

Every sync produces a **run** with a per-artifact **change** list (kind, slug, name,
action, direction, and any error). Runs are the audit trail for GitOps — they're
recorded in dedicated `git_sync_runs` / `git_sync_changes` tables, separate from the
web audit log. Use **dry-run** to preview exactly what a sync would change before
committing to it.

## Safety controls

GitOps defaults are deliberately conservative. The knobs worth knowing:

<AccordionGroup>
  <Accordion title="Secret scanning — secretPolicy">
    Before pushing, each artifact's `spec` is scanned for secret-like values. `block`
    (default) refuses the push, `warn` records a warning, `off` disables it. File
    payloads (base64 `content`) are skipped — they're the intended data and would
    always false-positive.
  </Accordion>

  <Accordion title="Matching — matchStrategy">
    `by_id` (default) matches artifacts only by their id, so importing can never
    silently bind, say, a credential to the wrong connection by name. Other strategies
    (`id_then_slug`, `by_slug`, `clone`) trade safety for flexibility.
  </Accordion>

  <Accordion title="Import behaviour — writeMode & dependencies">
    `writeMode` controls what an import does to a matched entity: `replace` (default)
    overwrites in place, `create` mints fresh ids, `version` appends a version row.
    `onMissingDependency` (`warn` / `fail`) decides what happens when an artifact
    references something not present.
  </Accordion>

  <Accordion title="Deletions — onUpstreamDelete & removeUntracked">
    `onUpstreamDelete` (`ignore_warn` default, or `soft_delete`) governs what happens
    when something disappears upstream. `removeUntracked` is a **destructive mirror**:
    a git→db sync deletes in-scope database artifacts that are absent from the repo.
    Off by default — enable only when the repo is truly the source of truth.
  </Accordion>

  <Accordion title="Same-deployment sync — allowSameDeployment">
    When two workspaces share one database, an imported artifact's canonical id may
    already exist elsewhere. `allowSameDeployment` clones it under a fresh local id
    instead of failing, and GitOps refuses cross-workspace references so tenants stay
    isolated. Generic-Git targets are also SSRF-guarded.
  </Accordion>
</AccordionGroup>

## Using GitOps in CI/CD

Because the repository holds your configuration as reviewable YAML, you can wrap it in
your normal delivery process:

* **Review config as code** — enable `db_to_git`, and every flow/agent change lands as
  a Git diff you can open a pull request against.
* **Promote across environments** — export from staging, then `git_to_db` into
  production (or `bidirectional` with `authoritative: git`) to roll a reviewed set of
  artifacts forward. `by_id` matching keeps entities stable across the hop.
* **Back up and restore** — a scheduled `db_to_git` sync gives you a point-in-time,
  versioned snapshot of a workspace you can restore by pulling.

<Tip>
  Start with `db_to_git` and `secretPolicy: block` to get a safe, one-way export
  working first. Only move to `git_to_db` / `bidirectional` — and only enable
  `removeUntracked` — once you've confirmed the repo contents with a **dry-run**.
</Tip>
