Skip to main content
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 Team Settings → API Keys and store it as a secret environment variable in your CI system (GitHub Actions secrets, GitLab CI variables, AWS Secrets Manager, etc.).

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:
DUVO_API_KEY="dv_..." duvo agents list --json
Or export it once at the top of your script:
export DUVO_API_KEY="${DUVO_API_KEY}"  # sourced from environment
duvo agents list --json
duvo runs start --agent "$AGENT_ID" --json

Rotating API keys

API keys do not expire by default. Rotate them by:
  1. Creating a new key at Team Settings → API Keys.
  2. Updating the key in your CI secret store.
  3. Deleting the old key from the dashboard.
There is a brief window between steps where both keys are valid — this ensures zero-downtime rotation. If a key is compromised, delete it immediately and treat any Jobs that ran under it as potentially untrusted.

GitHub Actions example

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 Job and wait for it to finish

duvo runs start returns immediately. Poll duvo runs get until the Job reaches a terminal status:
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 "Job failed with status: $STATUS" >&2
  exit 1
fi
echo "Job $RUN_ID completed."

Upload files before starting a Job

When your Assignment needs to process files, create a sandbox, upload the files, and pass the sandbox ID to the run:
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:
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-delegate Cases to an Assignment

When you need to route a set of Cases to a specific Assignment — for example, assigning a backlog of items after a new Assignment is deployed:
duvo cases bulk-delegate \
  --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 Job output from a completed run

After a Job finishes, pull the assistant’s messages to feed the output into downstream systems:
duvo runs messages "$RUN_ID" --json \
  | jq -r '[.messages[] | select(.role=="assistant") | .content] | join("\n")'

Roll out an Assignment config change to multiple Assignments

When a shared config file is updated (for example, a common SOP or tool set), push it as a new Revision across every affected Assignment:
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 Job trigger

Trigger a Job from any shell or pipeline and print the final output. Exit non-zero if the Job fails.
#!/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 Job: $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 "Job 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 Assignment config sync from Git

Store Assignment 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 Assignment 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):
name: Sync Assignments

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 Assignment 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 Assignment files in the repo.

Example 3: Weekly Job-status report

Run weekly in CI to summarize how many Jobs completed, failed, or are still running across your key Assignments. Post the summary wherever your team receives reports.
#!/usr/bin/env bash
set -euo pipefail

# Space-separated list of Assignment 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 Job 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

  • Always add --yes to bulk operations (bulk-delegate, 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.