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
}
| Param | Default | Max |
|---|---|---|
limit | 100 | 500 |
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" }
- Returns 202 if logs are not yet archived
- Returns 503 if log storage is not configured
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.
Related API Endpoints
GET /workspaces/{workspaceId}/runs/{runId}/logs/stream— SSE streamingGET /workspaces/{workspaceId}/runs/{runId}/logs— Paginated retrievalGET /runs/{id}/logs/json— Pollable JSON for CLI tailingGET /workspaces/{workspaceId}/runs/{runId}/logs/export— Presigned export URL