---
name: zc-cron
description: Manage, inspect, and test zync.is cron jobs hosted on cron-job.org
---

# /zc-cron — Zync Cron Job Management

## Overview

CF Workers free plan has a 5-cron limit (shared with other workers). All zync.is cron jobs run via **cron-job.org** HTTP triggers instead.

## Credentials

- **cron-job.org API key**: `/home/user/Projects/zync.is/secrets/cronjobs.api`
  - Current key: `HooYh3OwVavQEAqXULYaxvC47PRVfRgHylGHEdRew3c=`
  - **IP restriction enabled** — add the machine's outbound IP to the allowlist in cron-job.org Console → API Keys before API calls work
- **CRON_SECRET** (x-cron-secret header): in `/home/user/Projects/zync.is/Docs/worker-secrets.json`
  - Current value: `7460cd798627078fc986bfc83b9a332996faec4d7b01d1cc942806984e5bae2b`
- **Registered job IDs**: `/home/user/Projects/zync.is/Docs/cron-jobs.json` (written after registration)

## Target API

All jobs POST to: `https://zync-api.dry-salad-ffa1.workers.dev/api/cron/<name>`

All requests must include: `x-cron-secret: <CRON_SECRET>` header.

## Cron Schedule

| Schedule | Endpoints |
|---|---|
| Every 5 min | ticket-sla-check, session-expired-sweep, send-sequence-emails |
| Hourly | invoice-adapter-reconcile, tasks-sync |
| Daily 01:30 UTC | webhook-log-retention, audit-log-retention, data-retention-purge, audit-partition-create, session-idle-cleanup |
| Daily 05:00 UTC | subscription-trial-check, recurring-invoice-generator, proposal-expiry, proposal-expiry-reminder, calendar-sync, ticket-close-stale, contract-expiry-reminder, contract-signing-reminders, withholding-expiry-check |
| Daily 09:00 UTC | invoice-reminders, lead-reengagement, lead-score-refresh, recurring-task-generator, generate-recurring-expenses |

## Cron-job.org API Usage

```bash
# Auth header
AUTH="Authorization: Bearer $(FT_FROM_HOOK=1 ft cat /home/user/Projects/zync.is/secrets/cronjobs.api)"

# List all jobs
curl -s -H "$AUTH" https://api.cron-job.org/jobs | jq '.jobs[] | {id:.jobId, title:.title, enabled:.enabled}'

# Get job history (last executions)
curl -s -H "$AUTH" https://api.cron-job.org/jobs/<JOB_ID>/history | jq '.history[0]'

# Get job details
curl -s -H "$AUTH" https://api.cron-job.org/jobs/<JOB_ID> | jq '.'

# Create a job
curl -s -X PUT -H "$AUTH" -H "Content-Type: application/json" \
  https://api.cron-job.org/jobs \
  -d '{"job":{"url":"https://zync-api.dry-salad-ffa1.workers.dev/api/cron/NAME","title":"zync: NAME","enabled":true,"schedule":{"timezone":"UTC","hours":[-1],"mdays":[-1],"months":[-1],"wdays":[-1],"minutes":[0,5,10,15,20,25,30,35,40,45,50,55]},"requestMethod":1,"requestHeaders":[{"name":"x-cron-secret","value":"CRON_SECRET"}]}}'

# Delete a job
curl -s -X DELETE -H "$AUTH" https://api.cron-job.org/jobs/<JOB_ID>

# Manual trigger test (direct HTTP, no cron-job.org)
CRON_SECRET=$(FT_FROM_HOOK=1 ft cat /home/user/Projects/zync.is/Docs/worker-secrets.json | jq -r .CRON_SECRET)
curl -s -X POST \
  -H "x-cron-secret: $CRON_SECRET" \
  -H "Content-Type: application/json" \
  https://zync-api.dry-salad-ffa1.workers.dev/api/cron/session-expired-sweep
```

## Re-register All Jobs

NOTE: If re-registering, first delete existing "zync:" jobs to avoid duplicates.

The IP restriction on the API key must be resolved first. Log into cron-job.org → Console → API Keys → edit key → remove or update IP restriction.

## HTTP Endpoints Added (wave-13/14)

Three functions previously only callable via CF scheduled() now have HTTP routes:
- `POST /api/cron/session-expired-sweep` — deletes expired session rows
- `POST /api/cron/session-idle-cleanup` — revokes sessions idle beyond tenant timeout
- `POST /api/cron/lead-score-refresh` — enqueues lead score recalc jobs

Source files: `apps/zync-api/src/routes/cron/session-expired-sweep.ts`, `session-idle-cleanup.ts`, `lead-score-refresh.ts`

## Testing a Cron

```bash
CRON_SECRET=$(FT_FROM_HOOK=1 ft cat /home/user/Projects/zync.is/Docs/worker-secrets.json | jq -r .CRON_SECRET)
curl -s -X POST \
  -H "x-cron-secret: $CRON_SECRET" \
  -H "Content-Type: application/json" \
  https://zync-api.dry-salad-ffa1.workers.dev/api/cron/<endpoint-name>
```
Expected: `{"ok":true}` or `{"checked":true,...}` with HTTP 200.

---

## Learned Rules

### prefer-event-driven-over-cron | fired:1 | 2026-06-10
Reaching for a new cron/scheduled poll when an event-driven (webhook) path exists → wrong; project preference is to AVOID cron entirely where possible. Presenting "one new cron is fine" without that tradeoff missed the user's standing intent.
Prevent: before adding any cron, check whether a webhook/event trigger achieves the same; only fall back to cron when no event source exists, and surface the tradeoff.

