# JSON worker output

> Emit structured, one-object-per-line worker output for your log pipeline.

Source: https://boring-observability.dev/skyline/docs/json-worker-output
Section: Observability — Skyline for Laravel documentation
Updated: 2026-07-13

---

Setting `worker_output` to `json` makes Skyline's queue workers print **one structured JSON object per line** — job id, uuid, connection, queue, job class, status, attempts and duration — instead of the human-readable table. It is the format you want when your containers ship stdout straight to a log aggregator.

Each worker writes a line per job to its standard output either way. The default table is fine at a terminal and awkward in a pipeline that would rather parse a field than a column.

## Enabling

```php
// config/horizon.php
'worker_output' => env('HORIZON_WORKER_OUTPUT', 'cli'),
```

```ini
# .env
HORIZON_WORKER_OUTPUT=json
```

The value is trimmed and lower-cased before it is compared, so a stray `JSON` or a trailing space still opts in rather than silently falling back. Any value other than `json` means CLI output.

Because the setting is read by the worker process itself, a manually invoked `php artisan horizon:work` honours it too — you do not have to thread a flag through the supervisor.

> **Requires Laravel 11+**
>
> JSON output is built on the framework's structured worker output, which exists on Laravel 11 and later. On earlier versions the setting is never consulted and output stays in CLI format. No error is raised.

## Format

Each job produces a `starting` line when a worker picks it up and a terminal line when it finishes, on one line each:

```json
{"level":"info","id":"8813","uuid":"9f2c…","connection":"redis","queue":"default","job":"App\\Jobs\\ProcessPodcast","status":"starting","attempts":1,"timestamp":"2026-07-10T09:14:02.114302+00:00"}
{"level":"info","id":"8813","uuid":"9f2c…","connection":"redis","queue":"default","job":"App\\Jobs\\ProcessPodcast","status":"success","result":"deleted","attempts":1,"timestamp":"2026-07-10T09:14:04.882015+00:00","duration":2.767713}
```

| Field | Meaning |
| --- | --- |
| `level` | `info` for `starting` and `success`; `warning` otherwise. |
| `id` | The queue's job id — the same id used throughout the dashboard and in [lifecycle logging](https://boring-observability.dev/skyline/docs/job-lifecycle-logging). |
| `uuid` | The job's uuid. |
| `connection` | Queue connection name. |
| `queue` | Queue name. |
| `job` | Resolved job class name. |
| `status` | `starting`, `success`, `released_after_exception`, or `failed`. |
| `result` | `deleted`, `released` or `failed` — the job's disposition after the attempt. |
| `attempts` | Attempt number for this run. |
| `exception` | Exception class, on a failure. |
| `message` | Exception message, on a failure. |
| `duration` | Seconds the attempt took. Absent on the `starting` line. |
| `timestamp` | ISO-8601 timestamp with microseconds. |

Empty fields are omitted rather than emitted as `null`, so a successful job carries no `exception` key at all.

## Worker output or lifecycle logging?

They overlap, and they are not the same tool.

- **JSON worker output** is what the worker process prints to stdout. It is the natural fit when your containers ship stdout to a log aggregator, and it covers the job's execution — picked up, finished, how long it took.
- **[Job lifecycle logging](https://boring-observability.dev/skyline/docs/job-lifecycle-logging)** goes through Laravel's logging stack to a channel you choose, and covers events a worker never sees at all: a job being queued, a delayed job migrating, a dispatch discarded because a unique lock was held.

Both tag lines with the same job `id`, so if you enable both you can join across them. A common setup is JSON worker output for execution telemetry, plus a lifecycle channel at `warning` for the silent drops.


## Common questions

### How do I make Laravel queue workers output JSON?

Set worker_output to json in config/horizon.php (HORIZON_WORKER_OUTPUT=json). Each worker then writes one structured JSON object per line — job id, uuid, connection, queue, job class, status, attempts and duration — instead of the human-readable terminal table. It requires Laravel 11 or later.

### Should I use JSON worker output or job lifecycle logging?

They overlap, and they are not the same tool. JSON worker output is what the worker process prints to stdout and covers the job's execution — picked up, finished, how long it took — which suits containers shipping stdout to an aggregator. Lifecycle logging goes through Laravel's logging stack to a channel you choose, and covers events a worker never sees at all: a job being queued, a delayed job migrating, a dispatch discarded because a unique lock was held. Both tag lines with the same job id, so you can enable both and join across them.
