Forgecroft Docs
Runs

Logs

Three ways to access run logs: real-time streaming, paginated retrieval, and archived export.

Real-Time Streaming (SSE)

GET /workspaces/{workspaceId}/runs/{runId}/logs/stream

Streams log lines as Server-Sent Events. Each event:

id: 1
data: {"id":1,"ts":"2026-01-15T10:00:00Z","line":"Initializing provider..."}

id: 2
data: {"id":2,"ts":"2026-01-15T10:00:01Z","line":"Plan complete: 2 added, 1 changed"}

The stream ends with:

event: done
data: completed

The connection stays open and flushes new lines as they arrive. It terminates when the run reaches a terminal status or the client disconnects.

Paginated Log Retrieval

GET /workspaces/{workspaceId}/runs/{runId}/logs?limit=100&before=500

Returns log lines in reverse-chronological order (newest first):

{
  "lines": [
    {"id": 500, "ts": "2026-01-15T10:00:05Z", "line": "Apply complete"},
    {"id": 499, "ts": "2026-01-15T10:00:04Z", "line": "Applying changes..."}
  ],
  "oldest_id": 1,
  "has_more": true
}
ParamDefaultMax
limit100500
before(newest)

Use before with the oldest_id from the previous response to paginate backwards.

Polling for CLI Tailing

GET /runs/{id}/logs/json?after=0

Returns up to 500 lines after the given sequence number:

{
  "lines": [
    {"seq": 1, "line": "Initializing..."},
    {"seq": 2, "line": "Planning..."}
  ],
  "next_after": 2
}

Use next_after as the after param in the next request. Designed for CLI log tailing without SSE.

Complete polling example

RUN_ID="your-run-id"
AFTER=0

while true; do
  RESP=$(curl -s -H "Authorization: Bearer $FC_KEY" \
    "$FC_API/runs/$RUN_ID/logs/json?after=$AFTER")

  # Print new lines
  echo "$RESP" | jq -r '.lines[]?.line // empty'

  # Advance cursor
  NEW_AFTER=$(echo "$RESP" | jq -r '.next_after // empty')
  [ -n "$NEW_AFTER" ] && AFTER=$NEW_AFTER

  # Check if the run is done
  STATUS=$(curl -s -H "Authorization: Bearer $FC_KEY" \
    "$FC_API/runs/$RUN_ID" | jq -r '.status')
  case "$STATUS" in
    completed|failed|timed_out|rejected|cancelled|discarded|unlocked)
      echo "Run finished: $STATUS"
      break ;;
  esac

  sleep 2
done

Each request returns up to 500 lines. For runs that are still in progress, poll every 1-2 seconds. Once the run reaches a terminal status, no new lines will appear — drain any remaining lines and stop polling.

Or use the CLI: forgecroft runs logs --follow <run-id>

Exporting Logs

GET /workspaces/{workspaceId}/runs/{runId}/logs/export

Returns a presigned URL for the archived log blob:

{ "url": "https://r2.example.com/logs/run-uuid.gz" }

Archived Logs

For fully archived runs, paginated log retrieval seamlessly serves from R2 blob storage instead of Postgres. The API is the same — you don’t need to know where the logs are stored.

Secret Masking

Log lines appended by the runner have secrets automatically masked. The runner fetches secrets from the vault on first call and caches them per run. Secrets are purged after log archival.