---
title: Scheduled jobs
description: The jobs that must run on a schedule (store syncs, the CRM mailbox read, the product pipeline backfill and the backups), how each is triggered in production, how it is authorised, and where failures show up.
section: platform
order: 9
updated: 2026-10-23
---

# Scheduled jobs

A few things in Leitagu only happen when something calls them on a schedule: store connections sync, the CRM reads its mailbox, and the product pipeline backfill brings every product up to date. The apps have no scheduler of their own (no timers or cron libraries in the code), so each job is an HTTP endpoint that an outside scheduler calls. In production that scheduler is Coolify's **Scheduled Tasks**, which run a `curl` command inside the app's own container. This page lists each job as of 2026-10-23, including one that is not scheduled yet.

## Summary

| Job | Endpoint | App | Schedule in production | Status |
| --- | --- | --- | --- | --- |
| [Store sync](#store-sync) | `POST /api/cron/sync` | Partner | Coolify task `partner-hourly-sync`, `5 * * * *` (every hour at :05) | Running |
| [CRM mailbox read](#crm-mailbox-read) | `POST /api/cron/mail` | CRM | Coolify task `crm-mail-sync`, `*/5 * * * *` (every 5 minutes) | Running |
| [Product pipeline backfill](#product-pipeline-backfill) | `POST /api/cron/pipeline` | Partner | **None** | **Gap: not scheduled** |
| [Database backups](#backups) | (Coolify, no endpoint) | `leitagu-postgres` | Every 6 hours, and Coolify's own backup daily at 02:30 | Running |

Times are Estonian time (Europe/Tallinn), which is Coolify's instance time zone.

## How the jobs are triggered

- **Coolify Scheduled Tasks.** Each task belongs to one app in Coolify (the app's **Scheduled Tasks** tab) and runs its command in that app's running container, so it calls the app on its own port at `127.0.0.1` and never goes over the internet.
- **Auth.** Every `/api/cron/*` endpoint needs the header `Authorization: Bearer` followed by `CRON_SECRET`, compared in constant time; anything else gets 401. The task reads `$CRON_SECRET` from the container's environment, so the value is never written in the command. `CRON_SECRET` is set on the partner and CRM apps, and must be the same value on both.
- **GET also works** on every cron endpoint, for schedulers that can only make GET requests.
- **Failures.** The commands use `curl -fsS`, so an error status (401, 409, 500) or a timeout fails the task. Each run's status, duration and the endpoint's JSON answer are in the app's Scheduled Tasks tab in Coolify. No notification channel is set up in Coolify yet, so a failing task is only seen there (see [Known gaps](backups.md#known-gaps)).
- **Other hosting.** `deploy/vm/leitagu-sync.cron` has the same three calls as a cron file for the one-server setup in `DEPLOYMENT.md`. It is not used in production. The droplet's own crontab and systemd timers run nothing for Leitagu.

## Store sync

- **Endpoint.** `POST /api/cron/sync` on the partner app (`apps/partner/src/app/api/cron/sync/route.ts`).
- **What it does.** Runs every enabled Shopify, WooCommerce and feed connection that is due, one after another. Each connection's own schedule (hourly, daily or manual) decides whether it is due, so the endpoint must be called every hour. Synced products then go through the [product pipeline](product-pipeline.md). Answers `{"ran": <count>, "results": [{"id", "status"}]}`.
- **Production.** Coolify task `partner-hourly-sync` on the partner app, `5 * * * *`, timeout 1800 seconds:

  ```sh
  curl -fsS -m 1800 -X POST -H "Authorization: Bearer $CRON_SECRET" http://127.0.0.1:3002/api/cron/sync
  ```

  Scheduled since 2026-10-18. Last run before this page was written: 2026-10-23 00:05, succeeded in about 3.5 minutes; it synced three connections, all with the result `partial`.
- **Failures.** A connection that fails doesn't stop the others; the endpoint still answers 200 and lists its status. Each run is recorded in `sync_runs` with its errors and issues, shown on the connection in the partner app and on the store in the admin app. Connections that stop syncing show up as out of date in admin (see [Out-of-date data](../partners/integrations.md#out-of-date-data)).

## CRM mailbox read

- **Endpoint.** `POST /api/cron/mail` on the CRM (`apps/crm/src/app/api/cron/mail/route.ts`).
- **What it does.** Copies new emails from the shared mailbox's inbox and Sent folder into the CRM over IMAP (see [Migadu](technical-integrations.md#migadu)). Answers `{"stored": <count>, "folders": [...]}`; 409 `not_connected` when the mailbox settings are missing.
- **Production.** Coolify task `crm-mail-sync` on the CRM app, `*/5 * * * *`, timeout 150 seconds:

  ```sh
  curl -fsS -m 120 -X POST -H "Authorization: Bearer $CRON_SECRET" http://127.0.0.1:3003/api/cron/mail
  ```

  Scheduled since 2026-10-22. Runs take about 2 seconds; the last run before this page was written, at 2026-10-23 00:25, succeeded.
- **Failures.** A folder that can't be read keeps its error in `crm_mail_state`, shown in the CRM's Mailbox next to "Last checked"; the endpoint still answers 200, so the Coolify task stays green. "Check for new email" in the CRM reads the mailbox at once.

## Product pipeline backfill

- **Endpoint.** `POST /api/cron/pipeline` on the partner app (`apps/partner/src/app/api/cron/pipeline/route.ts`). Options: `?seconds=` (time budget, default 240), `?after=<next>` to continue, `?force=<stages>` or `?force=all`, `?limit=`.
- **What it does.** Runs the [product pipeline](product-pipeline.md) stages that are missing, out of date or of an older version, for every product, and answers with counts and `next` (null when it got through everything). `npm run pipeline:backfill` calls it in a loop.
- **Expected schedule.** Daily, and after every deploy (`deploy/vm/leitagu-sync.cron` uses 03:35 with `?seconds=1500`).
- **Production: not scheduled (gap).** There is no Coolify task for it, and the `images` deploy workflow doesn't call it after a deploy. Products still go through the pipeline when they are synced, created, edited or corrected, so day to day the gap shows only when a stage changes (a new stage or a version bump) or when a product's pipeline run failed: those products wait until someone runs the backfill by hand.
- **To add it.** A Coolify task on the partner app, for example `35 3 * * *` with a timeout of at least 1800 seconds:

  ```sh
  curl -fsS -m 1800 -X POST -H "Authorization: Bearer $CRON_SECRET" 'http://127.0.0.1:3002/api/cron/pipeline?seconds=1500'
  ```

  One call stops after its time budget; if `next` isn't null, the next day's run continues from the start, skipping what is already up to date.
- **Failures.** Products whose stages fail are counted as `failed` in the answer; products that need a person go to the review queue in the admin app.

## Backups

Coolify also runs the database backups on a schedule: `leitagu-postgres` every 6 hours (00:00, 06:00, 12:00, 18:00) and Coolify's own database daily at 02:30, both uploaded to Cloudflare R2. They show under the database's **Backups** tab. See [Backups and restore](backups.md).

## Related pages

- [Technical integrations](technical-integrations.md)
- [Backups and restore](backups.md)
- [Product pipeline](product-pipeline.md)
- [Partner integrations guide](../partners/integrations.md#sync-schedule)
