Skip to content

Scheduled jobs

Included free on every install, with email alerts for a failing job. Slack, Discord, PagerDuty and webhook alerts need a license with the alerts-pro feature. See pricing.

A scheduled job calls a URL of yours on a timetable. On each tick it sends a POST with a JSON body you choose, and it records whether the call worked, how long it took and what went wrong. Jobs belong to a tenant, and on several replicas each tick fires once, never once per replica.

Both run on a cron schedule. Choose by what has to happen on each tick.

Scheduled jobFlow with a cron trigger
What a tick doesOne POST to one URL with a fixed JSON bodyAny steps: read and write entries, branch, loop, call outside APIs, send email
Calling a URL outside the instanceFreeNeeds the flow-pro feature for the http.request node
RetriesBuilt in, see What a run doesSet on each request node with retries
Time zoneThe server's time zoneAny IANA zone you name, UTC by default
ScheduleFive fields, or six with seconds, at most once a minuteFive or six fields, or a descriptor such as @hourly
What a run recordsStatus, HTTP status code, duration, errorEvery step's input, output, timing and error
Counts toward the free limit of 20 flowsNoYes

Pick a scheduled job when your own service does the work and only needs to be told when. Pick a flow when the work is in LyEve's content or needs several steps. See flows.

You need an admin token in TOKEN. The quickstart shows how to get one. Replace the endpoint with a URL of yours that accepts a POST, such as a request inspector, so you can watch the calls arrive.

  1. Create a job that runs every day at 02:00:

    Terminal window
    curl -X POST http://localhost:3001/api/admin/cron/jobs \
    -H "Authorization: Bearer $TOKEN" \
    -H "Content-Type: application/json" \
    -d '{
    "name": "nightly-export",
    "schedule": "0 2 * * *",
    "endpoint": "https://hooks.example.com/export",
    "payload": {"format": "csv"}
    }'

    The answer is 201 with the job. Copy its id into JOB_ID:

    {
    "id": "3c0a5f9e-8d21-4b7e-9f13-6a2d4c8e1b07",
    "tenant_id": "default",
    "name": "nightly-export",
    "schedule": "0 2 * * *",
    "endpoint": "https://hooks.example.com/export",
    "payload": {"format": "csv"},
    "enabled": true,
    "created_at": "2026-10-01T10:00:00Z",
    "updated_at": "2026-10-01T10:00:00Z",
    "consecutive_failures": 0
    }
  2. Run it now instead of waiting for 02:00:

    Terminal window
    curl -X POST http://localhost:3001/api/admin/cron/jobs/$JOB_ID/trigger \
    -H "Authorization: Bearer $TOKEN"

    The answer is 202 with {"message": "job triggered"}. The call runs in the background, and your endpoint receives {"format": "csv"}.

  3. Read the run:

    Terminal window
    curl http://localhost:3001/api/admin/cron/jobs/$JOB_ID/history \
    -H "Authorization: Bearer $TOKEN"
    [
    {
    "id": "a41e7c2d-0b9f-4e35-8c6a-2f1d9e7b5c30",
    "job_id": "3c0a5f9e-8d21-4b7e-9f13-6a2d4c8e1b07",
    "tenant_id": "default",
    "status": "ok",
    "status_code": 200,
    "duration_ms": 184,
    "started_at": "2026-10-01T10:01:00Z",
    "finished_at": "2026-10-01T10:01:00Z"
    }
    ]

    A call that failed has "status": "error" and an error_message such as endpoint returned 405.

  4. Pause the schedule:

    Terminal window
    curl -X POST http://localhost:3001/api/admin/cron/jobs/$JOB_ID/disable \
    -H "Authorization: Bearer $TOKEN"

    The answer is the job with "enabled": false. /enable turns it back on.

  5. As a super admin, open Operations > Scheduled jobs in the admin console. The job is listed with its schedule, endpoint, status and last run.

A five-field expression is minute hour day-of-month month day-of-week:

ExpressionRuns
0 2 * * *Every day at 02:00
*/15 * * * *Every 15 minutes
0 9 * * 1Every Monday at 09:00
0 0 1 * *At midnight on the first of each month

A six-field expression adds seconds in front, such as 30 0 9 * * * for 09:00:30 every day. A schedule that would fire more than once a minute is refused. Times are read in the server's time zone.

Each run sends POST <endpoint> with Content-Type: application/json and the payload as the body, or an empty body when the job has no payload.

  • An answer below 400 marks the run ok and records the status code.
  • An answer of 400 or above, or a call that never connects, marks the run error. The reason is in error_message, such as endpoint returned 404.
  • A connection failure, a 429 or a 5xx is retried up to three times, waiting longer each time.
  • Each attempt times out after 30 seconds, and a run gives up after 60.
  • A call to a private, loopback or link-local address fails, so point jobs at a public address.

The job itself carries last_run_at and last_status once it has run, and consecutive_failures, the failed runs since its last success. Run history is kept for 30 days, and deleting a job deletes its history at once.

On every install, each failed run publishes the cron.job.failed event. A flow with an event trigger of kind: system and that name can act on it, for example to open a ticket. The event's data holds job_id, job_name, tenant_id, execution_id, status_code (0 when the endpoint gave no answer), error, attempt (the run's place in the current streak of failures, 1 for the first) and time (RFC 3339).

A job can email people once it fails a number of runs in a row, and send a recovery notice when it works again, on every install. With alerts-pro it can also post to Slack or Discord, open a PagerDuty incident or call a signed webhook. The alerts object goes in the job's body. Channels set while a license covered them keep firing after it lapses. See Alerts.

PUT /api/admin/cron/jobs/{id} replaces the job, so send name, schedule, endpoint and payload every time. A PUT that leaves out enabled turns the job on, and one that leaves out alerts keeps the stored channels. DELETE removes the job and its history and answers 204. The schedule changes as soon as the call returns, on every replica.

The ones you are most likely to meet:

  • 409 when another job in the tenant already has that name.
  • 422 with invalid cron expression: <reason> when the schedule does not parse.
  • 422 when the schedule fires more than once a minute.
  • 413 when the payload is over 64 KiB.
Every error the job routes return
StatusMessageCause
400invalid JSONThe body is not JSON, or has a field the job does not take.
400invalid inputname, schedule or endpoint is missing.
402payment_requiredAdding or changing a Slack, Discord, PagerDuty or webhook channel without alerts-pro. See Alerts.
404failed to get cron jobNo job with that id in your tenant.
409failed to create jobAnother job in the tenant has that name.
413payload exceeds maximum size: payload is <n> bytes, maximum is 65536 bytesThe payload is over 64 KiB.
422invalid cron expression: <reason>The schedule does not parse.
422schedule interval <interval> is below minimum 1m0s; sub-minute dispatch is not allowedThe schedule fires more than once a minute.
422endpoint must be an absolute http:// or https:// URLThe endpoint is relative or uses another scheme.
422endpoint must include a hostThe endpoint has no host.
422invalid alerts: ... or invalid alert channels: ..., such as invalid alerts: failure_threshold must be between 1 and 100An alerts value breaks a rule on Alerts. The message names the field.
503alert channels other than email cannot be stored right nowA Slack, Discord, PagerDuty or webhook channel on an instance without ENCRYPTION_KEY.

Scheduled jobs have no settings of their own. If you set LYEVE_PLUGINS to choose which features start, include cron in it. See licensing and tiers.

The /api/admin/cron/jobs routes need the admin or super_admin role. An admin token with the jobs:read or jobs:write grant can call them too.

Job routes
MethodPathPurpose
GET/api/admin/cron/jobsThe tenant's jobs as {"data": [...], "total_count": n, "limit": l, "offset": o}. limit defaults to 50, at most 500.
GET/api/admin/cron/jobs/{id}One job.
POST/api/admin/cron/jobsCreate a job: name, schedule, endpoint, optional description, payload, enabled (default true) and alerts. Answers 201.
PUT/api/admin/cron/jobs/{id}Replace a job. Send every field.
DELETE/api/admin/cron/jobs/{id}Delete a job and its history. Answers 204.
POST/api/admin/cron/jobs/{id}/triggerRun the job now. Answers 202.
POST/api/admin/cron/jobs/{id}/enableTurn the schedule on.
POST/api/admin/cron/jobs/{id}/disableTurn the schedule off.
GET/api/admin/cron/jobs/{id}/historyThe job's runs as a list, paginated with limit and offset.

The instance also serves /api/admin/jobs and /api/admin/jobs/{id} with GET, POST, PUT and DELETE, for super admins only. There the list is a plain array, PUT changes only the fields you send, and a missing field answers 400 with name, schedule, and endpoint are required.

  • Flows: scheduled work that runs inside LyEve, step by step.
  • Webhooks: a call to your URL when content changes.
  • Admin tokens: call the job routes from a script.