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

# Run Engini in CI/CD

> Headless authentication, exit-code branching, and JSON output in a pipeline.

<Info>**Uses:** CLI</Info>

The CLI is built for this: no interactive login required, stable exit codes, and JSON whenever output is piped.

## Authenticate without `login`

Skip `engini login` entirely - set the environment variable and the CLI picks it up:

```yaml GitHub Actions theme={null}
- name: Run an Engini tool
  env:
    ENGINI_API_KEY: ${{ secrets.ENGINI_API_KEY }}
  run: |
    npm install -g @engini/cli
    engini whoami --json
```

<Warning>
  Store the key as a masked secret. Never `engini login` in CI - it writes credentials to a config file on the runner.
</Warning>

## Branch on exit codes, not output

Every command exits with a documented code, so a pipeline can react precisely instead of grepping text:

```bash theme={null}
set +e
engini tools call inventory_sync --args @payload.json --json > result.json
code=$?
set -e

case $code in
  0)   echo "ok" ;;
  3)   echo "::error::Engini credentials invalid or expired"; exit 1 ;;
  4)   echo "::error::Tool or connection not found - was it renamed?"; exit 1 ;;
  5)   echo "::error::Validation failed"; jq -r '.message' result.json; exit 1 ;;
  124) echo "::warning::Timed out; the job may still be running server-side"; exit 0 ;;
  *)   echo "::error::Unexpected failure"; cat result.json; exit 1 ;;
esac
```

Exit `124` deserves that softer treatment: it means a **polled async job** didn't finish inside the timeout, not that it failed. The work often completes server-side afterwards, so failing the build on it causes false alarms.

## Parse with `jq`

Output is JSON automatically when piped - no `--json` needed, though it's harmless to be explicit:

```bash theme={null}
engini tools list --application monday | jq -r '.[].tool_slug'
engini connections list | jq -r '.[] | select(.is_alive == false) | .connection_id'
engini whoami | jq -r '.current_company'
```

## A nightly sync, end to end

```bash theme={null}
#!/usr/bin/env bash
set -euo pipefail

: "${ENGINI_API_KEY:?set ENGINI_API_KEY}"

# fail fast and loudly if the credential is dead
engini whoami --quiet || { echo "Engini auth failed"; exit 1; }

# warn on unhealthy connections before relying on them
unhealthy=$(engini connections list | jq -r '.[] | select(.is_alive == false) | .connection_name')
[ -n "$unhealthy" ] && echo "::warning::unhealthy connections: $unhealthy"

# do the work
engini tools call crm_export_contacts --args '{"since":"'"$(date -u -d '1 day ago' +%F)"'"}' --json > out.json
jq -r '.output | length' out.json
```

## Preview before you automate

`--dry-run` validates inputs and prints the request without executing - ideal on a pull request, so a bad payload fails review instead of production:

```bash theme={null}
engini tools call crm_export_contacts --args @payload.json --dry-run
```

## Discover flags programmatically

`--schema` returns each command's typed signature as JSON, so a generator or an agent can build invocations without scraping help text:

```bash theme={null}
engini tools call --schema | jq '.args | keys'
```

## Handling large outputs

By default, results over 4 KB spill to a local handle rather than flooding stdout. In CI you usually want the whole payload in a file:

```bash theme={null}
engini tools call list_rows --args '{}' --inline --json > rows.json   # force full inline
engini tools call list_rows --args '{}' --max-bytes 0 --json > rows.json  # equivalent
```

Leave the default in place when an **agent** is driving - that's when [drilling into a handle](/examples/large-results) beats paying for the whole payload.
