﻿# Sim Job Status

Poll `GET /v1/simc/jobs/{jobId}/status` to check on a job in flight. The response covers estimated time to start, the SimC progress percent, and a tail of recent SimC log lines. Skip to the [examples](#examples) for code samples.

## Response

`GET /v1/simc/jobs/{jobId}/status`

The following fields are included in a successful response. View in full in [API Reference](/docs/api-reference#tag/simc/GET/v1/simc/jobs/{id}/status).

**`status`** _(enum)_

The status of the job. Terminal values: `completed`, `failed`, `cancelled`, `timed_out`.

`pending` | `queued` | `starting` | `running` | `completed` | `failed` | `cancelled` | `timed_out`

**`errorCode`** _(enum | null)_

Machine-readable error code for the terminal state. Null when the job is not terminal or completed successfully.

`execution_interrupted` | `execution_timeout` | `execution_failed` | `build_unavailable` | `simulation_error` | `queue_timeout` | `input_invalid` | `insufficient_credits` | `user_cancelled` | `internal`

**`statusReason`** _(string | null)_

Human-readable explanation paired with `errorCode`. Null when the job is not terminal or no additional detail is available.

**`simcExitCode`** _(string | null)_

SimC process exit code. Null when the job is not terminal.

**`queue.queuedAt`** _(string)_

When this job entered the queue. For a retried job, this reflects the current attempt, not the original submission.

**`queue.estimatedStartSeconds`** _(number | null)_

Estimated seconds until this job begins running. Updated on each poll; not a guarantee. Null when no estimate is available.

**`queue.estimatedStartUpdatedAt`** _(string | null)_

When this estimate was last computed.

**`progress.percent`** _(number | null)_

Estimated completion percentage (0–100). Null when not yet running.

**`progress.stage`** _(object | null)_

Stage progress detail while the job is running. Null before the job starts running and once it reaches a terminal state. Shape: `{ current, total, label, percent }`. Single-pass jobs report `{ current: 1, total: 1, label: "initial" }` while running. See the [API Reference](/docs/api-reference#tag/simc/GET/v1/simc/jobs/{id}/status) for full field details.

**`logEntries`** _(array | null)_

Recent log lines from SimC stdout and stderr while the job is running. Each entry is tagged with its stream (`source`) and capture time (`ts`). Full logs are available as downloadable artifacts once the job completes. Null when not running or not requested via `include=logEntries`.

**`logEntries[].source`** _(enum)_

Which stream the line came from. `stderr` lines are diagnostic output: SimC warnings, errors, and notes. `stdout` lines are sim output, progress markers, and results.

`stdout` | `stderr`

**`logEntries[].message`** _(string)_

The log line text.

**`logEntries[].ts`** _(number)_

Epoch milliseconds (UTC) when this line was captured.

**`startedAt`** _(string | null)_

When this job began executing. Null when the job has not yet started.

**`updatedAt`** _(string | null)_

When this status was last updated.

**`retried`** _(boolean)_

True if this job was retried automatically because of a platform error. The response reflects the latest retry attempt.

## Polling

- Stop polling once `status` is terminal (`completed`, `failed`, `cancelled`, `timed_out`) and fetch the final result from `GET /v1/simc/jobs/{jobId}/result`.
- Aggressive polling can hit your read rate limit. Responses include `X-RateLimit-Remaining` and `X-RateLimit-Reset` headers.
- To skip polling entirely, subscribe to terminal job events via webhooks at submit time. See [Webhooks](/docs/api-advanced/webhooks).

## Examples

### Job time in queue

This example function logs a `queued` job's `queue.estimatedStartSeconds`, the estimated time until it starts running.

**Node.js**

```javascript
const secretKey = process.env.SIMMIT_SECRET_KEY

async function printJobQueueStatus(jobId) {
  // Send GET request to status endpoint
  const response = await fetch(
    `https://api.simmit.com/v1/simc/jobs/${jobId}/status`,
    { headers: { Authorization: `Bearer ${secretKey}` } }
  )

  if (!response.ok) {
    throw new Error(`Status failed: ${response.statusText}`)
  }

  const { status, queue } = await response.json()

  if (status === 'queued') {
    console.log(`Queue time estimate: ${queue.estimatedStartSeconds}s`)
  } else {
    console.log('Job is not in queue')
  }
}

// Use a real job ID
await printJobQueueStatus('123')
```

**Python**

```python
import os
import requests

secret_key = os.environ['SIMMIT_SECRET_KEY']

def print_job_queue_status(job_id):
    # Send GET request to status endpoint
    response = requests.get(
        f'https://api.simmit.com/v1/simc/jobs/{job_id}/status',
        headers={'Authorization': f'Bearer {secret_key}'}
    )

    if not response.ok:
        raise RuntimeError(f'Status failed: {response.reason}')

    body = response.json()

    if body['status'] == 'queued':
        print(f"Queue time estimate: {body['queue']['estimatedStartSeconds']}s")
    else:
        print('Job is not in queue')

# Use a real job ID
print_job_queue_status('123')
```

### Queue progress bar

Polling `status` lets you draw a progress bar for a `queued` job, computed as `elapsed / (elapsed + estimatedStartSeconds)` where `elapsed` is the time since `queue.queuedAt`. While the job waits, `estimatedStartSeconds` counts down at the same rate `elapsed` rises, so their sum (the estimated total wait) stays steady and the bar climbs smoothly between polls. The example clamps the percent so it never moves backwards (a live estimate can revise upward), restarts when an automatic retry re-enters the queue under a new `queuedAt`, and shows an indeterminate state when no estimate is available.

**Node.js**

```javascript
const secretKey = process.env.SIMMIT_SECRET_KEY

const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))

async function pollQueueProgress(jobId, { intervalMs = 5000 } = {}) {
  let lastPercent = 0 // clamp monotonic: a live ETA can revise upward
  let anchor = null // the queuedAt of the attempt we're tracking

  while (true) {
    const response = await fetch(
      `https://api.simmit.com/v1/simc/jobs/${jobId}/status`,
      { headers: { Authorization: `Bearer ${secretKey}` } }
    )

    if (!response.ok) {
      throw new Error(`Status failed: ${response.statusText}`)
    }

    const { status, queue } = await response.json()

    // The bar only applies while queued.
    if (status !== 'queued' || !queue) {
      console.log(`Job is now ${status}`)
      return status
    }

    const { queuedAt, estimatedStartSeconds } = queue

    // A new queuedAt means a new attempt (e.g. a retry): restart the bar.
    if (queuedAt !== anchor) {
      anchor = queuedAt
      lastPercent = 0
    }

    // No estimate yet: show an indeterminate bar instead of NaN.
    if (estimatedStartSeconds == null) {
      console.log('Queued (estimating)')
      await sleep(intervalMs)
      continue
    }

    const elapsedSeconds = (Date.now() - new Date(queuedAt).getTime()) / 1000
    const estimatedTotalSeconds = elapsedSeconds + estimatedStartSeconds

    // A near-zero estimate (job about to start) can zero the denominator;
    // show a near-full bar instead of dividing by zero.
    const raw =
      estimatedTotalSeconds > 0
        ? (elapsedSeconds / estimatedTotalSeconds) * 100
        : 99

    // Hold under 100% until the job starts; never go backwards.
    const percent = Math.min(99, Math.max(lastPercent, raw))
    lastPercent = percent

    console.log(`Queued: ${percent.toFixed(0)}%`)
    await sleep(intervalMs)
  }
}

// Use a real job ID
await pollQueueProgress('123')
```

**Python**

```python
import os
import time
from datetime import datetime

import requests

secret_key = os.environ['SIMMIT_SECRET_KEY']

def poll_queue_progress(job_id, interval_seconds=5):
    last_percent = 0  # clamp monotonic: a live ETA can revise upward
    anchor = None  # the queuedAt of the attempt we're tracking

    while True:
        response = requests.get(
            f'https://api.simmit.com/v1/simc/jobs/{job_id}/status',
            headers={'Authorization': f'Bearer {secret_key}'}
        )

        if not response.ok:
            raise RuntimeError(f'Status failed: {response.reason}')

        body = response.json()
        status = body['status']
        queue = body.get('queue')

        # The bar only applies while queued.
        if status != 'queued' or not queue:
            print(f'Job is now {status}')
            return status

        queued_at = queue['queuedAt']
        estimated_start_seconds = queue['estimatedStartSeconds']

        # A new queuedAt means a new attempt (e.g. a retry): restart the bar.
        if queued_at != anchor:
            anchor = queued_at
            last_percent = 0

        # No estimate yet: show an indeterminate bar instead of NaN.
        if estimated_start_seconds is None:
            print('Queued (estimating)')
            time.sleep(interval_seconds)
            continue

        queued_at_seconds = datetime.fromisoformat(
            queued_at.replace('Z', '+00:00')
        ).timestamp()
        elapsed_seconds = time.time() - queued_at_seconds
        estimated_total_seconds = elapsed_seconds + estimated_start_seconds

        # A near-zero estimate (job about to start) can zero the denominator;
        # show a near-full bar instead of dividing by zero.
        if estimated_total_seconds > 0:
            raw = elapsed_seconds / estimated_total_seconds * 100
        else:
            raw = 99

        # Hold under 100% until the job starts; never go backwards.
        percent = min(99, max(last_percent, raw))
        last_percent = percent

        print(f'Queued: {percent:.0f}%')
        time.sleep(interval_seconds)

# Use a real job ID
poll_queue_progress('123')
```

### Job progress percent

This example function logs the `progress.percent`. The progress percent is an estimate, derived from the sim's running output.

**Node.js**

```javascript
const secretKey = process.env.SIMMIT_SECRET_KEY

async function printJobProgress(jobId) {
  // Send GET request to status endpoint
  const response = await fetch(
    `https://api.simmit.com/v1/simc/jobs/${jobId}/status`,
    { headers: { Authorization: `Bearer ${secretKey}` } }
  )

  if (!response.ok) {
    throw new Error(`Status failed: ${response.statusText}`)
  }

  const { status, progress } = await response.json()

  if (status !== 'running') {
    console.log(`Job is ${status}`)
    return
  }

  if (progress.percent != null) {
    console.log(`Progress: ${progress.percent.toFixed(1)}%`)
  }
}

// Use a real job ID
await printJobProgress('123')
```

**Python**

```python
import os
import requests

secret_key = os.environ['SIMMIT_SECRET_KEY']

def print_job_progress(job_id):
    # Send GET request to status endpoint
    response = requests.get(
        f'https://api.simmit.com/v1/simc/jobs/{job_id}/status',
        headers={'Authorization': f'Bearer {secret_key}'}
    )

    if not response.ok:
        raise RuntimeError(f'Status failed: {response.reason}')

    body = response.json()

    if body['status'] != 'running':
        print(f"Job is {body['status']}")
        return

    if body['progress']['percent'] is not None:
        print(f"Progress: {body['progress']['percent']:.1f}%")

# Use a real job ID
print_job_progress('123')
```

### Job SimC log tail

Pass `?include=logEntries` to the status endpoint to get the most recent
`stdout`/`stderr` lines while SimC is running. Each entry is tagged with its
stream (`source`) and capture time (`ts`). `logEntries` is `null` unless the
job is running and you requested it.

**Node.js**

```javascript
const secretKey = process.env.SIMMIT_SECRET_KEY

async function printRecentLogs(jobId) {
  const response = await fetch(
    // Pass `?include=logEntries`
    `https://api.simmit.com/v1/simc/jobs/${jobId}/status?include=logEntries`,
    { headers: { Authorization: `Bearer ${secretKey}` } }
  )

  if (!response.ok) {
    throw new Error(`Status failed: ${response.statusText}`)
  }

  const { status, logEntries } = await response.json()

  if (status !== 'running') {
    console.log(`Job is ${status}`)
    return
  }

  for (const { source, message, ts } of logEntries ?? []) {
    const time = new Date(ts).toISOString()
    console.log(`[${time}] ${source}: ${message}`)
  }
}

// Use a real job ID
await printRecentLogs('123')
```

**Python**

```python
import os
from datetime import datetime, timezone
import requests

secret_key = os.environ['SIMMIT_SECRET_KEY']

def print_recent_logs(job_id):
    response = requests.get(
        # Pass `?include=logEntries`
        f'https://api.simmit.com/v1/simc/jobs/{job_id}/status?include=logEntries',
        headers={'Authorization': f'Bearer {secret_key}'}
    )

    if not response.ok:
        raise RuntimeError(f'Status failed: {response.reason}')

    body = response.json()

    if body['status'] != 'running':
        print(f"Job is {body['status']}")
        return

    for entry in body.get('logEntries') or []:
        time = datetime.fromtimestamp(
            entry['ts'] / 1000, tz=timezone.utc
        ).isoformat(timespec='milliseconds').replace('+00:00', 'Z')
        print(f"[{time}] {entry['source']}: {entry['message']}")

# Use a real job ID
print_recent_logs('123')
```

To actually _tail_, poll on an interval and track the last `ts` you've seen, filtering entries at or below it. Stop polling once `status` is terminal (`completed`, `failed`, `cancelled`, `timed_out`).

### Job is finished

A job is terminal once its `status` is `completed`, `failed`, `cancelled`, or `timed_out`. At that point, fetch the result from `GET /v1/simc/jobs/{jobId}/result`.

**Node.js**

```javascript
const secretKey = process.env.SIMMIT_SECRET_KEY

const TERMINAL_JOB_STATUSES = ['completed', 'failed', 'cancelled', 'timed_out']

async function printIsJobTerminal(jobId) {
  // Send GET request to status endpoint
  const response = await fetch(
    `https://api.simmit.com/v1/simc/jobs/${jobId}/status`,
    { headers: { Authorization: `Bearer ${secretKey}` } }
  )

  if (!response.ok) {
    throw new Error(`Status failed: ${response.statusText}`)
  }

  const { status } = await response.json()

  if (TERMINAL_JOB_STATUSES.includes(status)) {
    console.log('Job is terminal, time to fetch the result')
  } else {
    console.log('Job is not terminal')
  }
}

// Use a real job ID
await printIsJobTerminal('123')
```

**Python**

```python
import os
import requests

secret_key = os.environ['SIMMIT_SECRET_KEY']

TERMINAL_JOB_STATUSES = {'completed', 'failed', 'cancelled', 'timed_out'}

def print_is_job_terminal(job_id):
    response = requests.get(
        f'https://api.simmit.com/v1/simc/jobs/{job_id}/status',
        headers={'Authorization': f'Bearer {secret_key}'}
    )

    if not response.ok:
        raise RuntimeError(f'Status failed: {response.reason}')

    body = response.json()

    if body['status'] in TERMINAL_JOB_STATUSES:
        print('Job is terminal, time to fetch the result')
    else:
        print('Job is not terminal')

# Use a real job ID
print_is_job_terminal('123')
```

### Job polling

This example function polls a job's `status` on a fixed interval, logging queue ETA or running progress on each tick, until the job is terminal. It then fetches the result.

**Node.js**

```javascript
const secretKey = process.env.SIMMIT_SECRET_KEY

const TERMINAL_JOB_STATUSES = ['completed', 'failed', 'cancelled', 'timed_out']

const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))

async function pollJob(jobId, { intervalMs = 2000 } = {}) {
  while (true) {
    const response = await fetch(
      `https://api.simmit.com/v1/simc/jobs/${jobId}/status`,
      { headers: { Authorization: `Bearer ${secretKey}` } }
    )

    if (!response.ok) {
      throw new Error(`Status failed: ${response.statusText}`)
    }

    const { status, queue, progress } = await response.json()

    if (TERMINAL_JOB_STATUSES.includes(status)) {
      console.log(`Job ${status}`)
      return status
    }

    if (status === 'queued' && queue?.estimatedStartSeconds != null) {
      console.log(`Queued (eta ~${queue.estimatedStartSeconds}s)`)
    } else if (status === 'running' && progress.percent != null) {
      console.log(`Running (${progress.percent.toFixed(1)}%)`)
    }

    await sleep(intervalMs)
  }
}

// Use a real job ID
const jobId = '123'

// Poll until the job is terminal, then fetch the result
const status = await pollJob(jobId)
if (status === 'completed') {
  const result = await fetch(
    `https://api.simmit.com/v1/simc/jobs/${jobId}/result`,
    { headers: { Authorization: `Bearer ${secretKey}` } }
  ).then(r => r.json())
  console.log(`DPS: ${result.result.summary.mainActor.mean}`)
}
```

**Python**

```python
import os
import time
import requests

secret_key = os.environ['SIMMIT_SECRET_KEY']

TERMINAL_JOB_STATUSES = {'completed', 'failed', 'cancelled', 'timed_out'}

def poll_job(job_id, interval_seconds=2):
    while True:
        response = requests.get(
            f'https://api.simmit.com/v1/simc/jobs/{job_id}/status',
            headers={'Authorization': f'Bearer {secret_key}'}
        )

        if not response.ok:
            raise RuntimeError(f'Status failed: {response.reason}')

        body = response.json()
        status = body['status']

        if status in TERMINAL_JOB_STATUSES:
            print(f'Job {status}')
            return status

        queue = body.get('queue') or {}
        progress = body.get('progress') or {}

        if status == 'queued' and queue.get('estimatedStartSeconds') is not None:
            print(f"Queued (eta ~{queue['estimatedStartSeconds']}s)")
        elif status == 'running' and progress.get('percent') is not None:
            print(f"Running ({progress['percent']:.1f}%)")

        time.sleep(interval_seconds)

# Use a real job ID
job_id = '123'

# Poll until the job is terminal, then fetch the result
status = poll_job(job_id)

if status == 'completed':
    result = requests.get(
        f'https://api.simmit.com/v1/simc/jobs/{job_id}/result',
        headers={'Authorization': f'Bearer {secret_key}'}
    ).json()
    print(f"DPS: {result['result']['summary']['mainActor']['mean']}")
```

---

_HTML version: https://docs.staging.simmit.gg/docs/learning/simc-job-status · Full docs index: https://docs.staging.simmit.gg/llms.txt_
