Skip to content

GitHub Actions ​

Call a Console agent from a GitHub Actions workflow with nothing but curl and jq, both preinstalled on GitHub-hosted runners. The workflow creates a run, long-polls it, reads the result, and cancels the run if the job is cancelled. The same pattern as Azure DevOps, in YAML.

When to use it: review a pull request with an agent, draft release notes at tag time, triage a failing test run, or gate a deployment on an agent's verdict.

Secrets ​

In the repository (or organisation) go to Settings → Secrets and variables → Actions and add:

NameKindValue
CONSOLE_URLVariablehttps://console.example.com
CONSOLE_TOKENSecretA cpeer_… token of a dedicated service user with the agents service

The Console URL must be reachable from the runner. Self-hosted Console behind a firewall needs a self-hosted runner in the same network. No inbound access is needed, because the workflow polls.

Reusable pattern ​

Put the create-and-wait logic in one composite step so each workflow stays short. This script takes the agent key, the input text and an optional JSON response_format, and writes the run to run.json.

bash
#!/usr/bin/env bash
# .github/scripts/console-run.sh <agent-key> <input-file> [response_format.json]
set -euo pipefail
BASE="$CONSOLE_URL/api/client/v1"
AUTH="Authorization: Bearer $CONSOLE_TOKEN"
KEY="${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}-${3:-plain}-$1"      # run id + attempt + step

BODY=$(jq -n --rawfile input "$2" \
  --arg repo "$GITHUB_REPOSITORY" --arg run "$GITHUB_RUN_ID" \
  '{input: $input, metadata: {repo: $repo, github_run: $run}}')
if [ -n "${3:-}" ]; then
  BODY=$(jq --slurpfile rf "$3" '. + {response_format: $rf[0]}' <<<"$BODY")
fi

RUN=$(curl -fsS -X POST "$BASE/agents/$1/runs" -H "$AUTH" -H "Content-Type: application/json" \
  -H "Idempotency-Key: $KEY" -d "$BODY")
RUN_ID=$(jq -r .id <<<"$RUN")
echo "CONSOLE_RUN_ID=$RUN_ID" >> "$GITHUB_ENV"                # for the cancel step
echo "Run $RUN_ID"

# Long-poll: each request is held up to 50 s by the server and returns as soon as the run settles.
for _ in $(seq 1 40); do                                       # about 30+ minutes at most
  STATUS=$(jq -r .status <<<"$RUN")
  case "$STATUS" in scheduled|queued|running) ;; *) break ;; esac
  RUN=$(curl -fsS --max-time 70 "$BASE/agents/runs/$RUN_ID?wait=50" -H "$AUTH")
done

echo "$RUN" > run.json
STATUS=$(jq -r .status run.json)
if [ "$STATUS" != "succeeded" ]; then
  echo "::error::Agent run $RUN_ID is $STATUS: $(jq -r '.error.message // "no error message"' run.json)"
  exit 1
fi

Example: review a pull request and comment on it ​

yaml
# .github/workflows/agent-review.yml
name: Agent PR review
on:
  pull_request:
    types: [opened, synchronize]

permissions:
  contents: read
  pull-requests: write

concurrency:
  group: agent-review-${{ github.event.pull_request.number }}
  cancel-in-progress: true            # a new push cancels the job, and the cancel step stops the run

jobs:
  review:
    runs-on: ubuntu-latest
    timeout-minutes: 35
    env:
      CONSOLE_URL: ${{ vars.CONSOLE_URL }}
      CONSOLE_TOKEN: ${{ secrets.CONSOLE_TOKEN }}
    steps:
      - uses: actions/checkout@v4
        with: { fetch-depth: 0 }

      - name: Build the review request
        env:
          PR_TITLE: ${{ github.event.pull_request.title }}     # via env, never interpolated into the script
          BASE_REF: ${{ github.base_ref }}
        run: |
          {
            echo "Review this pull request. Title: $PR_TITLE"
            echo
            git diff "origin/$BASE_REF...HEAD" -- . ':!*.lock' | head -c 60000
          } > request.txt
          cat > schema.json <<'JSON'
          { "type": "json_schema",
            "json_schema": { "name": "review", "strict": true, "schema": {
              "type": "object", "additionalProperties": false,
              "required": ["verdict", "summary", "findings"],
              "properties": {
                "verdict":  { "type": "string", "enum": ["approve", "comment", "request_changes"] },
                "summary":  { "type": "string" },
                "findings": { "type": "array", "items": { "type": "object", "additionalProperties": false,
                  "required": ["file", "severity", "note"],
                  "properties": { "file": {"type":"string"}, "severity": {"type":"string","enum":["low","medium","high"]}, "note": {"type":"string"} } } }
              } } } }
          JSON

      - name: Run the agent
        run: bash .github/scripts/console-run.sh code-reviewer request.txt schema.json

      - name: Post the review as a comment
        env:
          GH_TOKEN: ${{ github.token }}
          PR_NUMBER: ${{ github.event.pull_request.number }}
        run: |
          VERDICT=$(jq -r '.output_parsed.verdict' run.json)
          {
            echo "### Agent review: **$VERDICT**"
            echo
            jq -r '.output_parsed.summary' run.json
            echo
            jq -r '.output_parsed.findings[] | "- **\(.severity)** `\(.file)`: \(.note)"' run.json
            echo
            echo "<sub>Console run \`$(jq -r .id run.json)\`</sub>"
          } > comment.md
          gh pr comment "$PR_NUMBER" --body-file comment.md

      - name: Job summary
        if: always()
        run: |
          [ -f run.json ] || exit 0
          {
            echo "## Agent review"
            echo "- Run: \`$(jq -r .id run.json)\` ($(jq -r .status run.json))"
            echo "- Verdict: $(jq -r '.output_parsed.verdict // "n/a"' run.json)"
          } >> "$GITHUB_STEP_SUMMARY"

      - name: Fail on a blocking verdict
        run: test "$(jq -r '.output_parsed.verdict' run.json)" != "request_changes"

      - name: Cancel the run if the job was cancelled
        if: cancelled()
        run: |
          [ -n "${CONSOLE_RUN_ID:-}" ] || exit 0
          curl -sS -X POST "$CONSOLE_URL/api/client/v1/agents/runs/$CONSOLE_RUN_ID/cancel" \
            -H "Authorization: Bearer $CONSOLE_TOKEN" >/dev/null || true

Notes:

  • if: cancelled() runs when the workflow is cancelled (by a person, by concurrency, or by a timeout) and stops the Console run, so a superseded review does not keep working. A run that already finished answers 409 agent_run_already_terminal; the || true ignores it.
  • The idempotency key has the run id and the attempt, so "Re-run failed jobs" repeats the step without double-starting a turn within an attempt, and a new attempt starts a fresh run.
  • Keep the diff inside the agent's context. The example truncates at 60 000 characters; for large changes, put the diff in a file and send it as an input_file part, or summarise per directory.
  • Fork pull requests do not receive secrets. Use pull_request_target carefully (never check out untrusted code with secrets), or run the review from a trusted workflow triggered after the PR workflow.
  • Cost control. Console applies the agent's session budget and the tenant spend limit; a run that hits them ends with a clear stop reason. Tag runs with metadata as above to find them in Console.

Example: release notes on tag ​

yaml
name: Release notes
on:
  push:
    tags: ['v*']
jobs:
  notes:
    runs-on: ubuntu-latest
    env:
      CONSOLE_URL: ${{ vars.CONSOLE_URL }}
      CONSOLE_TOKEN: ${{ secrets.CONSOLE_TOKEN }}
    steps:
      - uses: actions/checkout@v4
        with: { fetch-depth: 0 }
      - run: |
          PREV=$(git describe --tags --abbrev=0 "${GITHUB_REF_NAME}^" 2>/dev/null || git rev-list --max-parents=0 HEAD)
          git log --pretty='- %s (%h)' "$PREV..$GITHUB_REF_NAME" > request.txt
      - run: bash .github/scripts/console-run.sh release-notes request.txt
      - run: jq -r .output_text run.json > NOTES.md
      - uses: softprops/action-gh-release@v2
        with: { body_path: NOTES.md }

Without a workflow: a webhook trigger ​

If the agent only has to react to an event, such as a pull request being opened, you do not need a workflow at all: add a webhook trigger to the agent and set its URL as a repository (or organization) webhook. GitHub posts the event, Console starts the run and the agent comments with its own GitHub tool. Use a workflow when the job should wait for the verdict and fail the check, a webhook trigger when it should not; the recipe shows a PR review without any workflow.

Without polling: a callback ​

GitHub-hosted runners cannot receive inbound requests, so polling is the right default. If you want the pipeline to end and a separate workflow to react to the result, send a callback to a small relay that calls GitHub's repository_dispatch API, or subscribe a webhook to agent_run.* and have the relay forward it. Most pipelines should simply wait; see the use cases.

Studio · Pulse · Console · Agent SDK and more — the Cognipeer documentation hub