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

# Scripting and CI/CD Patterns

> Run the Duvo CLI in scripts and CI/CD pipelines with non-interactive authentication, JSON output, and worked automation examples.

The Duvo CLI is designed to run in scripts and automation pipelines, not just interactively. This page covers how to authenticate in non-interactive environments, common automation patterns, and three complete worked examples.

## Authentication in CI/CD and scripts

### Use an API key, not OAuth

OAuth sessions require a browser login and are bound to a user's session. For CI/CD pipelines and server-side scripts, use an API key instead.

Generate a key at [Your Profile → API keys](https://app.duvo.ai/settings/profile#api-keys) — scoped to a single team or to all teams you can access — and store it as a secret environment variable in your CI system (GitHub Actions secrets, GitLab CI variables, AWS Secrets Manager, etc.). Users with the Manager role or above can also create and view a team's keys at [Team Settings → API keys](https://app.duvo.ai/settings/api-keys).

### Pass the key as an environment variable

The CLI reads `DUVO_API_KEY` automatically, so you never need to touch a config file or run `duvo login` in a pipeline:

<CodeGroup>
  ```bash Inline theme={"dark"}
  DUVO_API_KEY="dv_..." duvo agents list --json
  ```

  ```bash Exported theme={"dark"}
  export DUVO_API_KEY="${DUVO_API_KEY}"  # sourced from environment
  duvo agents list --json
  duvo runs start --agent "$AGENT_ID" --json
  ```
</CodeGroup>

### Rotating API keys

API keys do not expire by default. Rotate them by:

<Steps>
  <Step title="Create a new key" icon="key">
    Create a new key at [Your Profile → API keys](https://app.duvo.ai/settings/profile#api-keys).
  </Step>

  <Step title="Update the CI secret" icon="lock">
    Update the key in your CI secret store.
  </Step>

  <Step title="Delete the old key" icon="trash">
    Delete the old key from the dashboard.
  </Step>
</Steps>

There is a brief window between steps where both keys are valid — this ensures zero-downtime rotation.

<Warning>
  If a key is compromised, delete it immediately and treat any Runs that ran under it as potentially untrusted.
</Warning>

### GitHub Actions example

```yaml theme={"dark"}
jobs:
  duvo-sync:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm install -g @duvoai/cli
      - run: duvo agents list --json
        env:
          DUVO_API_KEY: ${{ secrets.DUVO_API_KEY }}
```

***

## Common scripting patterns

### Start a Run and wait for it to finish

`duvo runs start` returns immediately. Poll `duvo runs get` until the Run reaches a terminal status:

```bash theme={"dark"}
RUN_ID=$(duvo runs start \
  --agent "$AGENT_ID" \
  --message "Process the daily batch." \
  --json | jq -r '.run.id')

while true; do
  STATUS=$(duvo runs get "$RUN_ID" --json | jq -r '.run.status')
  case "$STATUS" in
    completed|failed|stopped) break ;;
  esac
  sleep 10
done

if [ "$STATUS" != "completed" ]; then
  echo "Run failed with status: $STATUS" >&2
  exit 1
fi
echo "Run $RUN_ID completed."
```

### Upload files before starting a Run

When your Agent needs to process files, create a sandbox, upload the files, and pass the sandbox ID to the run:

```bash theme={"dark"}
SANDBOX_ID=$(duvo sandboxes create --json | jq -r '.sandbox.id')

duvo sandboxes upload "$SANDBOX_ID" ./input-data.csv

RUN_ID=$(duvo runs start \
  --agent "$AGENT_ID" \
  --sandbox-id "$SANDBOX_ID" \
  --json | jq -r '.run.id')
```

For files larger than 10 MB, get a presigned URL and upload directly:

```bash theme={"dark"}
UPLOAD=$(duvo sandboxes prepare-upload-url "$SANDBOX_ID" \
  --path /workspace/data.csv --json)
URL=$(echo "$UPLOAD" | jq -r '.upload_url')
curl -X PUT --data-binary "@./data.csv" "$URL"
```

### Bulk-reprocess Cases on an Agent

When you need to route a set of Cases to a specific Agent — for example, assigning a backlog of items after a new Agent is deployed.

<Warning>
  Any active Runs for the specified Cases are interrupted.
</Warning>

```bash theme={"dark"}
duvo cases bulk-reprocess \
  --queue "$QUEUE_ID" \
  --agent "$AGENT_ID" \
  --ids "case-1,case-2,case-3,case-4,case-5" \
  --yes
```

`--ids` accepts a comma-separated list of up to 100 Case IDs. The `--yes` flag skips the confirmation prompt, which is required in non-interactive scripts.

### Collect Run output from a completed run

After a Run finishes, pull the assistant's messages to feed the output into downstream systems:

```bash theme={"dark"}
duvo runs messages "$RUN_ID" --json \
  | jq -r '[.messages[] | select(.role=="assistant") | .content] | join("\n")'
```

### Roll out an Agent config change to multiple Agents

When a shared config file is updated (for example, a common AOP or tool set), push it as a new Revision across every affected Agent:

```bash theme={"dark"}
REVISION_NAME="config-update-$(date +%Y%m%d)"

for AGENT_ID in agent-id-1 agent-id-2 agent-id-3; do
  duvo revisions create \
    --agent "$AGENT_ID" \
    --name "$REVISION_NAME" \
    --config-file ./shared-config.json
  echo "Updated $AGENT_ID"
done
```

***

## Worked examples

### Example 1: One-shot Run trigger

Trigger a Run from any shell or pipeline and print the final output. Exit non-zero if the Run fails.

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

AGENT_ID="$1"          # pass as argument: ./trigger-job.sh <agent-id>
MESSAGE="${2:-}"       # optional message

RUN_ID=$(duvo runs start \
  --agent "$AGENT_ID" \
  ${MESSAGE:+--message "$MESSAGE"} \
  --json | jq -r '.run.id')

echo "Started Run: $RUN_ID"

while true; do
  STATUS=$(duvo runs get "$RUN_ID" --json | jq -r '.run.status')
  case "$STATUS" in
    completed|failed|stopped) break ;;
  esac
  sleep 10
done

echo "Run finished: $STATUS"

# Print the last assistant message
duvo runs messages "$RUN_ID" --json \
  | jq -r '.messages[] | select(.role=="assistant") | .content' \
  | tail -1

[ "$STATUS" = "completed" ] || exit 1
```

### Example 2: Nightly Agent config sync from Git

Store Agent configs as JSON files in a Git repository and push any changed configs to Duvo on every merge to `main`. This lets you version-control your Agent Setups alongside your application code.

**Repository layout:**

```
assignments/
  invoice-processor.json
  order-tracker.json
  supplier-follow-up.json
agent-ids.env          # INVOICE_PROCESSOR_ID=abc123 ...
```

**Sync script (`.github/workflows/sync-assignments.yml`):**

```yaml theme={"dark"}
name: Sync Agents

on:
  push:
    branches: [main]
    paths:
      - "assignments/**"

jobs:
  sync:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 2 # need HEAD and HEAD^ to diff

      - run: npm install -g @duvoai/cli

      - name: Push changed configs
        env:
          DUVO_API_KEY: ${{ secrets.DUVO_API_KEY }}
        run: |
          source agent-ids.env
          CHANGED=$(git diff --name-only HEAD^ HEAD -- assignments/)

          for FILE in $CHANGED; do
            NAME=$(basename "$FILE" .json)
            VAR_NAME=$(echo "$NAME" | tr '[:lower:]-' '[:upper:]_')_ID
            AGENT_ID="${!VAR_NAME:-}"

            if [ -z "$AGENT_ID" ]; then
              echo "No Agent ID for $NAME — skipping."
              continue
            fi

            duvo revisions create \
              --agent "$AGENT_ID" \
              --name "git-$(git rev-parse --short HEAD)" \
              --config-file "$FILE"

            echo "Synced $NAME ($AGENT_ID)"
          done
```

Only files changed in the push are synced, so the workflow is fast even with many Agent files in the repo.

### Example 3: Weekly Run-status report

Run weekly in CI to summarize how many Runs completed, failed, or are still running across your key Agents. Post the summary wherever your team receives reports.

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

# Space-separated list of Agent IDs to include in the report
AGENTS="agent-id-1 agent-id-2 agent-id-3"

completed=0
failed=0
stopped=0

for AGENT_ID in $AGENTS; do
  RUNS=$(duvo runs list --agent "$AGENT_ID" --limit 100 --json 2>/dev/null \
    || echo '{"runs":[]}')

  completed=$((completed + $(echo "$RUNS" | jq '[.runs[] | select(.status=="completed")] | length')))
  failed=$((failed     + $(echo "$RUNS" | jq '[.runs[] | select(.status=="failed")] | length')))
  stopped=$((stopped   + $(echo "$RUNS" | jq '[.runs[] | select(.status=="stopped")] | length')))
done

total=$((completed + failed + stopped))

echo "Weekly Run Summary"
echo "=================="
echo "Completed : $completed / $total"
echo "Failed    : $failed / $total"
echo "Stopped   : $stopped / $total"
```

Pipe the output to `slack-cli`, `mail`, or any notification tool your team uses.

***

## Tips

<Tip>
  * Always add `--yes` to bulk operations (`bulk-reprocess`, `bulk-update-status`, `cases delete`) in scripts so they don't block waiting for confirmation.
  * Combine `--json` with `jq` for all scripting — human-readable output can change between CLI versions, but JSON is stable.
  * Set `DUVO_PROFILE` to target a non-default profile (e.g., staging) without changing your shell's default: `DUVO_PROFILE=staging duvo runs start --agent "$AGENT_ID" --json`.
  * Run `duvo <command> --help` to see the full flag set for any command.
</Tip>

## Related

<CardGroup cols={2}>
  <Card title="Starting Runs" href="/cli/running-jobs">
    Full command reference for `duvo runs`
  </Card>

  <Card title="Managing Agents" href="/cli/managing-assignments">
    Creating Agents and Revisions
  </Card>

  <Card title="Cases and Queues" href="/cli/cases-and-queues">
    Bulk Case operations
  </Card>

  <Card title="Advanced Commands" href="/cli/advanced">
    `duvo api`, sandboxes, and raw API access
  </Card>

  <Card title="Public API Reference" href="../api-reference">
    REST alternative if you need more than the CLI offers
  </Card>
</CardGroup>
