> ## Documentation Index
> Fetch the complete documentation index at: https://docs.theaitracker.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Jobs and scheduling

> What runs automatically, when, and how to operate it.

## The schedule

Defined in `routes/console.php`. Every entry is `withoutOverlapping()`, `onOneServer()` and
`runInBackground()`, and pinned to UTC so the cadence does not drift with server timezone.

| Time (UTC) | Command                   | Frequency |
| ---------- | ------------------------- | --------- |
| 01:00      | `sentiment:fetch-daily`   | Daily     |
| 02:15      | `traffic:fetch-yesterday` | Daily     |
| 03:00      | `weekly:prompt-batch`     | Daily     |
| 04:00      | `bot-access:refresh`      | Monthly   |

<Note>
  `weekly:prompt-batch` has run daily since July 2026. The command name was left alone so
  existing deployments, runbooks and log filters kept working — treat the name as historical.
</Note>

For the schedule to fire, the host needs the single Laravel cron entry:

```
* * * * * cd /path/to/app && php artisan schedule:run >> /dev/null 2>&1
```

`onOneServer()` requires a shared cache store across web nodes. The default `CACHE_STORE` is
`database`, which satisfies this as long as all nodes share the database.

## Manual commands

Beyond the scheduled four:

| Command                                | Purpose                                                      |
| -------------------------------------- | ------------------------------------------------------------ |
| `traffic:backfill`                     | Backfill historical GA4 traffic for campaigns                |
| `traffic:fetch-yesterday --campaign=*` | Re-fetch traffic for specific campaigns                      |
| `ga4:fetch-timezone {campaign_id?}`    | Cache a GA4 property's timezone                              |
| `competitors:classify`                 | Classify discovered competitors via LLM                      |
| `competitors:score`                    | Recalculate industry visibility scores                       |
| `dispatch:serpapi`                     | SerpApi-only dispatch with debug logging                     |
| `clerk:migrate-users`                  | Import local users into Clerk, linking by `external_auth_id` |
| `onboarding:bench-variants`            | Benchmark LLM speed and quality for brand-variant generation |

## Queue workers

`QUEUE_CONNECTION` defaults to `database`. `config/queues.php` sits alongside Laravel's own
`config/queue.php` and holds the application's queue naming, so check both when routing a job
to a specific worker.

In production, run a supervised worker rather than `queue:listen`:

```bash theme={null}
php artisan queue:work --tries=3 --timeout=300
```

Provider calls are slow — batch fetches in particular — so keep the timeout generous. Jobs
that exceed it are retried, which means duplicate provider spend.

## Batch behaviour

`config/ai.php` controls batch submission:

| Variable                          | Default | Effect                                     |
| --------------------------------- | ------- | ------------------------------------------ |
| `AI_BATCH_ENABLED`                | `true`  | Master switch for batched submission       |
| `AI_BATCH_MAX_REQUESTS_PER_BATCH` | `1000`  | Requests per submitted batch               |
| `AI_BATCH_TIMEOUT_HOURS`          | `24`    | How long to wait before abandoning a batch |

Not every provider supports batching. OpenAI's entry sets `'batch_supported' => false`
deliberately — the comment in `config/ai.php` notes it was disabled in favour of realtime
processing — so OpenAI requests are issued directly regardless of `AI_BATCH_ENABLED`.

## Failure handling

* Failed jobs land in the `failed_jobs` table; `php artisan queue:retry all` re-dispatches.
* `PromptRunFailureAlert` surfaces batches where runs failed.
* With `MATTERMOST_LOG_ENABLED=true`, errors are posted to the configured Mattermost channel
  through the custom log channel in `config/logging.php` and `app/Logging`.

<Tip>
  When a scheduled command appears to do nothing, check `withoutOverlapping()` first. A
  previous run that died without releasing its lock will silently block subsequent runs until
  the lock expires.
</Tip>
