# Report CI and deploy progress to Taskhold

> Have a GitHub Actions job open a Taskhold task, plan its stages as steps, tick them off and block with the logs link on failure. Plain curl and jq.

Source: https://taskhold.com/docs/ci-progress
Last updated: 2026-10-26

Anything that can make an HTTP request can report into Taskhold, not only coding agents. A deploy job that shows up on the live board next to your agents means one place to look. This recipe uses the REST API from GitHub Actions; the same calls work from any CI system or a cron job.

## Before you begin

- Create an API key in Taskhold Settings and name it after the job, for example "CI deploy".
- Add it to the repository as the secret TASKHOLD_API_KEY.
- The X-Taskhold-Agent header sets the name shown on the board, here "GitHub Actions (deploy)".

## The workflow

```yaml
# .github/workflows/deploy.yml (excerpt): one Taskhold task per run, one step per stage.
jobs:
  deploy:
    runs-on: ubuntu-latest
    env:
      TH: https://taskhold.com/api
      AUTH: "Authorization: Bearer ${{ secrets.TASKHOLD_API_KEY }}"
      AGENT: "X-Taskhold-Agent: GitHub Actions (${{ github.workflow }})"
    steps:
      - uses: actions/checkout@v4

      - name: Open a Taskhold task with steps
        id: th
        run: |
          json() { curl -sf -H "$AUTH" -H "$AGENT" -H "Content-Type: application/json" "$@"; }
          task=$(json "$TH/tasks" -d "{\"title\": \"Deploy ${GITHUB_SHA::7}\", \"priority\": \"high\"}" | jq -r .task.id)
          json -X PATCH "$TH/tasks/$task" -d '{"status": "in_progress"}' > /dev/null
          read -r build test ship < <(json "$TH/tasks/$task/subtasks" \
            -d '{"steps": ["Build", "Test", "Ship"]}' | jq -r '[.subtasks[].id] | join(" ")')
          echo "task=$task" >> "$GITHUB_OUTPUT"
          echo "build=$build" >> "$GITHUB_OUTPUT"
          echo "test=$test" >> "$GITHUB_OUTPUT"
          echo "ship=$ship" >> "$GITHUB_OUTPUT"

      - name: Build
        run: |
          curl -sf -X PATCH "$TH/tasks/${{ steps.th.outputs.build }}" -H "$AUTH" -H "$AGENT" \
            -H "Content-Type: application/json" -d '{"status": "in_progress"}' > /dev/null
          make build
          curl -sf -X PATCH "$TH/tasks/${{ steps.th.outputs.build }}" -H "$AUTH" -H "$AGENT" \
            -H "Content-Type: application/json" -d '{"status": "done"}' > /dev/null

      # ... the same for Test and Ship ...

      - name: Tell the person if it failed
        if: failure()
        run: |
          url="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID"
          curl -sf -X PATCH "$TH/tasks/${{ steps.th.outputs.task }}" -H "$AUTH" -H "$AGENT" \
            -H "Content-Type: application/json" \
            -d "{\"status\": \"blocked\", \"blocked_reason\": \"Deploy failed. Logs: $url\"}"
```

## What each call does

| Call | Effect on the board |
| --- | --- |
| POST /api/tasks | A new task, marked as added by an agent |
| PATCH /api/tasks/{id} with status in_progress | The card moves to Working now with the job's name and a start time |
| POST /api/tasks/{id}/subtasks with steps | The bar splits into one segment per stage |
| PATCH a step with status done | That segment fills; the bar advances by one stage |
| PATCH the task with status blocked and blocked_reason | The card moves to Needs you with the logs link |

For a long stage, report counts inside it with POST /api/tasks/{step_id}/progress and a body like {"current": 120, "total": 400, "message": "Uploading assets"}.

## Related

- [Taskhold REST API: tasks, steps, progress, curl examples](https://taskhold.com/docs/rest-api.md)
- [How live progress works: steps, bars, ETA and liveness](https://taskhold.com/docs/live-progress.md)
- [Slack alerts for blocked tasks from the events stream](https://taskhold.com/docs/slack-alerts.md)
