# Get cron run

`GET https://api.instacloud.com/projects/{projectId}/cron-jobs/{id}/runs/{runId}`

**Token scope:** `account` · `org` · `project` — read-only tokens allowed.

Read one run and every attempt it made.

## Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `projectId` | path | `string` | yes |  |
| `id` | path | `string` | yes |  |
| `runId` | path | `string` | yes |  |

## Response 200

Default Response

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `run` | `object` | yes |  |
| `run.id` | `string` | yes |  |
| `run.schedule_id` | `string` | yes |  |
| `run.scheduled_at` | `string` | yes | the tick this run belongs to (manual runs: when it was asked for) |
| `run.trigger_type` | `"scheduled" \| "manual"` | yes |  |
| `run.schedule_revision` | `integer` | yes | the schedule as of dispatch; a later edit does not change a run in flight |
| `run.status` | `"queued" \| "preparing" \| "sending" \| "retry_wait" \| "succeeded" \| "failed" \| "unknown" \| "cancelled" \| "skipped"` | yes | the terminal status IS the result — there is no separate outcome |
| `run.skip_reason` | `"overlap" \| "late" \| null` | yes |  |
| `run.skipped_count` | `integer` | yes |  |
| `run.dispatch_generation` | `integer` | yes |  |
| `run.dispatched_at` | `string \| null` | yes |  |
| `run.execution_epoch` | `integer` | yes | bumped per attempt; a superseded worker’s late result is dropped against it |
| `run.platform_retry_count` | `integer` | yes |  |
| `run.application_retry_count` | `integer` | yes |  |
| `run.next_attempt_at` | `string \| null` | yes |  |
| `run.started_at` | `string \| null` | yes |  |
| `run.finished_at` | `string \| null` | yes |  |
| `run.created_at` | `string` | yes |  |
| `run.config` | `object` | yes |  |
| `run.config.target` | `object` | yes | Where the job sends its request. An internal target names the SERVICE, never a URL: the worker resolves the live route at send time, so a redeploy or a region move cannot leave a job firing at a dead host. |
| `run.config.method` | `"GET" \| "POST"` | yes |  |
| `run.config.headerNames` | `string[]` | yes | names only, literal and secret-backed alike — never values |
| `run.config.secretRefs` | `object` | yes | header name → secret NAME, as frozen for this run |
| `run.config.requestTimeoutMs` | `integer` | yes |  |
| `run.config.retry` | `object` | yes |  |
| `run.config.retry.platformMaxRetries` | `integer` | no | retries for failures where nothing was sent (wake/transport). Default 2. |
| `run.config.retry.applicationMaxRetries` | `integer` | no | retries for a non-2xx answer. Default 0 — a broken endpoint should not be hammered. |
| `run.config.retry.retryableStatuses` | `integer[]` | no | e.g. [429, 503]; empty means none |
| `run.config.retry.maxRunAgeMs` | `integer` | no | give up on the run once it is this old, whatever the retry budget says |
| `attempts` | `object[]` | yes |  |
| `attempts.id` | `string` | yes |  |
| `attempts.run_id` | `string` | yes |  |
| `attempts.attempt_no` | `integer` | yes |  |
| `attempts.execution_epoch` | `integer` | yes |  |
| `attempts.started_at` | `string` | yes |  |
| `attempts.finished_at` | `string \| null` | yes |  |
| `attempts.wake_ms` | `integer \| null` | yes | time spent waking the target, excluded from the request timeout |
| `attempts.request_ms` | `integer \| null` | yes |  |
| `attempts.failure_kind` | `"wake_failed" \| "blocked" \| "http_error" \| "timeout" \| "transport" \| "interrupted" \| null` | yes |  |
| `attempts.error_code` | `string \| null` | yes |  |
| `attempts.http_status` | `integer \| null` | yes |  |
| `attempts.response_excerpt` | `string \| null` | yes |  |

## Response 404

Default Response

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `error` | `string` | yes |  |
