Setup recipes

Connect the tools
you already use.

Step-by-step guides with code you can copy: schedule requests to your app, watch the scripts and sites you run, and send alerts to the place your team already reads.

These are setup guides, not account connections. CronEngine never signs in to Laravel, WordPress, GitHub, Vercel, Slack or any other service. Each recipe uses a normal cron job, heartbeat, uptime monitor or alert channel, with an address and, where needed, a credential you create for it.

Schedule requests

We call your URL on a schedule.

Watch your scripts

Your script pings us when it finishes.

Watch your sites

We check your site from our side.

Alerts and status pages

Tell your team, then your customers.

Keep integration credentials separate. CronEngine encrypts authentication secrets and custom header values before storing them, under a key held outside the database. Use a unique, limited token for each integration and revoke it if the job or URL is exposed. A heartbeat’s ping address works the same way: anyone who has it can ping, so keep it out of public code.

Schedule requests

We call your URL on a schedule.

For work your app can do when asked. You give CronEngine an address, and it calls that address at the times you choose and records what came back.

Laravel Scheduler

Bridge Laravel’s scheduler through a protected route.

Laravel normally runs schedule:run from a server cron every minute. CronEngine cannot execute Artisan directly, so your application needs a small HTTPS route that checks a unique token before calling it.

CronEngine job settings

URL
https://your-app.com/cronengine/schedule
Method
POST
Authentication
Bearer token
Schedule
* * * * *
Overlap protection
On
Laravel evaluates due tasks when schedule:run is called. A complete Laravel Scheduler bridge therefore needs every-minute execution, which currently fits Business and Enterprise plans. For lower plans, expose and schedule one specific queued task instead. Sub-minute Laravel schedules are not a good fit for an HTTP bridge.

Already running the scheduler on your own server? Then you do not need this route. Watch it with a heartbeat instead.

Laravel scheduler documentation ↗
Laravel route
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Route;

Route::post('/cronengine/schedule', function (Request $request) {
    $expected = (string) config('services.cronengine.secret');
    $provided = (string) $request->bearerToken();

    abort_if(
        $expected === '' || ! hash_equals($expected, $provided),
        403
    );

    $exitCode = Artisan::call('schedule:run');

    return response()->json(
        ['ok' => $exitCode === 0],
        $exitCode === 0 ? 200 : 500
    );
});
Secret configuration
// config/services.php
'cronengine' => [
    'secret' => env('CRONENGINE_SECRET'),
],

// .env
CRONENGINE_SECRET=generate-a-long-unique-token
WordPress

Run WP-Cron without waiting for visitors.

Disable WordPress’s page-load trigger only after the external CronEngine job is ready. CronEngine then requests wp-cron.php on a predictable interval.

While you are at it, add an uptime monitor for the site itself, so you also hear about it when the site goes down.

Open the complete WordPress guide

Quick recipe

1. wp-config.php

define( 'DISABLE_WP_CRON', true );

Place this before the “stop editing” comment, after confirming the external job works.

2. CronEngine job

URL
https://your-site.com/wp-cron.php?doing_wp_cron
Method
GET
Schedule
Every 15 minutes on Free or every 5 minutes on Starter

Choose an interval that matches how much delay your scheduled posts and plugin tasks can tolerate. If a firewall blocks wp-cron.php, allow CronEngine’s documented outbound IP.

WordPress external scheduler documentation ↗
GitHub Actions

Dispatch a workflow from CronEngine.

Add the workflow_dispatch event to the workflow, then call GitHub’s workflow-dispatch endpoint with a token that has Actions write access to that repository.

CronEngine job settings

URL
https://api.github.com/repos/OWNER/REPO/actions/workflows/WORKFLOW_FILE/dispatches
Method
POST
Authentication
Bearer token
Content type
application/json
Success
Default 2xx rule

Why scheduled workflows run late, how to create the token and what each error means: read the full guide.

GitHub workflow API documentation ↗
.github/workflows/cronengine.yml
name: CronEngine task

on:
  workflow_dispatch:

jobs:
  scheduled-task:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: ./scripts/nightly-task.sh
Custom headers
Accept: application/vnd.github+json
X-GitHub-Api-Version: 2026-03-10
JSON body
{
  "ref": "main"
}

Replace WORKFLOW_FILE with the workflow file name, such as cronengine.yml. The ref must name a branch or tag that contains the workflow.

Vercel

Call a protected function or schedule a deployment.

For application work, expose a route that checks a bearer secret. For a scheduled rebuild, create a Vercel Deploy Hook and let CronEngine send a POST request to its generated URL.

Protected function

Set CRON_SECRET in Vercel, use the same value as CronEngine’s Bearer token, and call your production route with GET.

Deploy Hook

Create it under Project Settings → Git → Deploy Hooks. Use POST with no body. The unique hook URL can trigger deployments, so treat the URL itself as a secret.

On the Hobby plan Vercel cron runs at most once a day. The limits per plan and the ways around them: read the full guide.

app/api/cron/route.js
export function GET(request) {
  const authorization = request.headers.get('authorization');

  if (
    !process.env.CRON_SECRET ||
    authorization !== `Bearer ${process.env.CRON_SECRET}`
  ) {
    return new Response('Unauthorized', { status: 401 });
  }

  // Queue or perform your scheduled work here.
  return Response.json({ success: true });
}

Function job

https://your-app.vercel.app/api/cron

GET · Bearer token · no body

Deploy Hook job

https://api.vercel.com/v1/integrations/deploy/...

POST · no authentication field · no body

Any HTTP API

Use the same request options with your own endpoint.

CronEngine supports GET, POST, PUT, PATCH, DELETE, and HEAD. Add custom headers, a JSON or text body where supported, Basic or Bearer authentication, and success rules that match the endpoint. Already have a working curl command? Paste it into the job form and it fills itself in.

  • Return a 2xx response when the request was accepted.
  • Queue long-running work and respond quickly when possible.
  • Use a unique secret and compare it before doing any work.
  • Keep overlap protection on unless concurrent calls are safe.

Example request

POST
URL
https://api.example.com/tasks/reindex
Authentication
Bearer token
Custom header
X-Job-Source: cronengine
Success rule
2xx status codes
JSON body
{
  "source": "cronengine",
  "task": "refresh-search-index"
}

Watch your scripts

Your script pings us. We notice when it stops.

For work that runs where we cannot reach: a crontab on your server, a queue worker, a CI job. Add one line at the end, and if the ping does not arrive on time, you get an alert. How cron monitoring works.

One address, three endings.

Every heartbeat gets its own private address. Request it with any method, from any tool that can make a web request. It answers in plain text, so curl -fsS stays quiet when it works.

  • Pings are free and never count against your scheduled runs.
  • A grace period keeps a slow night from counting as a failure.
  • Send /start first and each run’s duration is recorded.
  • Use the test button on the heartbeat to see your first ping arrive.
https://cronengine.com/ping/hb_… Finished

The run is done and went well. This is the one that resets the clock.

https://cronengine.com/ping/hb_…/start Started

The run began. Optional, but it lets us measure how long each run takes.

https://cronengine.com/ping/hb_…/fail Failed

The run broke. You get an alert straight away, without waiting for the grace period.

Shell and crontab

Watch a backup, an import, anything in your crontab.

The shortest version is one line: run the task, and only if it succeeds, ping. A failed task sends nothing, the ping goes missing, and you hear about it once the grace period is over.

CronEngine heartbeat settings

Expect a ping
On a schedule: 0 3 * * *
Grace period
Longer than the task usually takes
Timezone
The same as your server

-m 10 stops a slow network from hanging the script, --retry 3 survives a short blip, and -o /dev/null keeps cron from mailing you the reply.

crontab
# Ping when the backup finishes. The && means a failed backup sends nothing,
# so the ping goes missing and you get an alert.
0 3 * * * /usr/local/bin/backup.sh && curl -fsS -m 10 --retry 3 -o /dev/null https://cronengine.com/ping/hb_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
backup-with-ping.sh (start, finish and fail)
#!/bin/sh
PING=https://cronengine.com/ping/hb_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

curl -fsS -m 10 --retry 3 -o /dev/null "$PING/start"

if /usr/local/bin/backup.sh; then
  curl -fsS -m 10 --retry 3 -o /dev/null "$PING"
else
  curl -fsS -m 10 --retry 3 -o /dev/null "$PING/fail"
fi
Laravel

Watch a scheduled Artisan command.

If your server already runs schedule:run, Laravel can ping for you. Give each important command its own heartbeat, so you know exactly which one stopped.

This also catches the scheduler itself going quiet, for example after a server move where nobody set the crontab up again.

Laravel pinging URLs documentation ↗
routes/console.php
// routes/console.php
use Illuminate\Support\Facades\Schedule;

$ping = config('services.cronengine.backup_ping');

Schedule::command('backup:run')
    ->dailyAt('03:00')
    ->pingBefore("{$ping}/start")
    ->pingOnSuccess($ping)
    ->pingOnFailure("{$ping}/fail");
Ping address configuration
// config/services.php
'cronengine' => [
    'backup_ping' => env('CRONENGINE_BACKUP_PING'),
],

// .env
CRONENGINE_BACKUP_PING=https://cronengine.com/ping/hb_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
GitHub Actions

Know when a scheduled workflow did not run.

GitHub’s own scheduled workflows can start late or be skipped when GitHub is busy, and a failed run is easy to miss in a long list. A heartbeat tells you either way.

Store the ping address as a repository secret named CRONENGINE_PING, so it never ends up in your code.

The CronEngine Heartbeat action does the pinging for you, masks the address in the logs and never fails your workflow if we cannot be reached. Plain curl works just as well.

Prefer CronEngine to start the workflow on time instead? Use the workflow dispatch recipe above.

.github/workflows/nightly-export.yml (with the action)
name: Nightly export

on:
  schedule:
    - cron: '0 3 * * *'

jobs:
  export:
    runs-on: ubuntu-latest
    steps:
      - uses: cronengine-com/heartbeat-action@v1
        with:
          ping-url: ${{ secrets.CRONENGINE_PING }}
          status: start
      - uses: actions/checkout@v4
      - run: ./scripts/export.sh
      - if: always()
        uses: cronengine-com/heartbeat-action@v1
        with:
          ping-url: ${{ secrets.CRONENGINE_PING }}
          status: ${{ job.status }}
.github/workflows/nightly-export.yml (with curl)
name: Nightly export

on:
  schedule:
    - cron: '0 3 * * *'

jobs:
  export:
    runs-on: ubuntu-latest
    env:
      PING: ${{ secrets.CRONENGINE_PING }}
    steps:
      - uses: actions/checkout@v4
      - run: curl -fsS -m 10 --retry 3 -o /dev/null "$PING/start"
      - run: ./scripts/export.sh
      - if: success()
        run: curl -fsS -m 10 --retry 3 -o /dev/null "$PING"
      - if: failure()
        run: curl -fsS -m 10 --retry 3 -o /dev/null "$PING/fail"
From your own code

Python, Node.js and PowerShell.

The same pattern in each: ping at the start, ping when it works, ping /fail when it breaks. Each one swallows its own network errors, so a ping that cannot get out never stops the actual job.

import.py
import urllib.request

PING = "https://cronengine.com/ping/hb_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"

def ping(suffix=""):
    try:
        urllib.request.urlopen(PING + suffix, timeout=10)
    except OSError:
        pass  # never let the ping break the job itself

ping("/start")
try:
    run_import()
    ping()
except Exception:
    ping("/fail")
    raise
worker.mjs
// worker.mjs (Node.js 18 or newer)
const PING = 'https://cronengine.com/ping/hb_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX';

const ping = (suffix = '') =>
  fetch(PING + suffix, { signal: AbortSignal.timeout(10_000) })
    .catch(() => {}); // never let the ping break the job itself

await ping('/start');
try {
  await runImport();
  await ping();
} catch (err) {
  await ping('/fail');
  throw err;
}
backup-task.ps1 (Windows Task Scheduler)
$ErrorActionPreference = 'Stop'
$ping = 'https://cronengine.com/ping/hb_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'

Invoke-RestMethod "$ping/start" -TimeoutSec 10 | Out-Null
try {
    & 'C:\Scripts\backup.ps1'
    Invoke-RestMethod $ping -TimeoutSec 10 | Out-Null
} catch {
    Invoke-RestMethod "$ping/fail" -TimeoutSec 10 | Out-Null
    throw
}

Watch your sites

We check your site. You hear when it stops answering.

Give an uptime monitor an address and we request it from our side, as often as every minute on Pro and up, and judge every answer by the rules you set. How uptime monitoring works.

Check the page that proves it works.

A homepage can load fine while checkout or the database is broken. If you can, point the monitor at a health check that tests what your site actually needs, and have it answer with an error when something is wrong.

  • Checks are GET or HEAD, with no headers, body or login, so the address has to be public.
  • Every check gives up after 10 seconds, on every plan.
  • By default any 2xx or 3xx answer counts as up. Narrow it to the codes you expect.
  • Add words the page must contain, to catch an error page that still answers 200.
  • Wait for several failed checks in a row before it alerts, so one blip wakes nobody.
  • Set a slow limit, and a check that answers later than that is marked slow.

Laravel 11 and newer

New Laravel apps already have a health route at /up. It answers 200 when the app boots and 500 when it does not.

URL
https://your-app.com/up
Method
GET
Success
200

WordPress

Check the homepage and require a word that only appears when the page really rendered, such as your site name.

URL
https://your-site.com/
Method
GET
Must contain
Your Site Name
Express (Node.js)
// A health check that tests what the site needs,
// not only that the web server is running.
app.get('/health', async (req, res) => {
  try {
    await db.query('SELECT 1');
    res.json({ status: 'ok' });
  } catch {
    res.status(503).json({ status: 'down' });
  }
});
Plain PHP
<?php
// public/health.php
try {
    $pdo = new PDO($dsn, $user, $pass, [PDO::ATTR_TIMEOUT => 3]);
    $pdo->query('SELECT 1');
    echo 'ok';
} catch (Throwable $e) {
    http_response_code(503);
    echo 'down';
}

From the Starter plan up, the same monitor also watches the site’s certificate and emails you before it expires. If your firewall only lets known addresses in, allow CronEngine’s documented outbound IP.

Alerts and status pages

Tell your team first. Then your customers.

Every cron job, heartbeat and uptime monitor can send its alerts to one or more channels. A status page then tells everyone else.

Email

Sent to the notification address in your settings.

  1. 1Set and confirm the address under Settings, if it differs from your login.
  2. 2Pick Email as a channel on the job or monitor.

Slack

Posts into the channel of your choice.

  1. 1In Slack, create an incoming webhook for the channel.
  2. 2In CronEngine, add a Webhook channel and choose the Slack format.
  3. 3Paste the URL, which starts with https://hooks.slack.com/services/.

Discord

Posts into a Discord channel.

  1. 1In the channel settings, open Integrations, then Webhooks.
  2. 2Create a webhook and copy its URL.
  3. 3Add a Webhook channel with the Discord format and paste it.

Telegram

A message from your own bot, in a chat or group.

  1. 1Create a bot with @BotFather and copy its token.
  2. 2Add the bot to the chat or group that should get the alerts.
  3. 3Enter the token and the chat ID, such as -1001234567890 or @yourchannel.

Your own webhook

A signed JSON message to any address you run.

  1. 1Add a Webhook channel with the generic format.
  2. 2Optionally set a signing secret to verify each request.
  3. 3Handle the event types listed on the right.

Event types

Sent in the type field and the X-CronEngine-Event-Type header.

job.failed job.recovered job.run.succeeded job.run.failed monitor.down monitor.recovered uptime.down uptime.recovered certificate.expiring certificate.issuer_changed

Verify that a webhook came from us.

Every request to your own webhook carries X-CronEngine-Timestamp. With a signing secret set, it also carries X-CronEngine-Signature, which is v1= followed by an HMAC-SHA256 of the timestamp, a dot and the raw body, made with your secret. Compare it before you trust the message.

How many channels one job or monitor can use at once depends on your plan, from one on Free to three on Business and up.

Read the webhook guide
Example: uptime.down
{
  "version": "1",
  "id": "evt_48213",
  "type": "uptime.down",
  "created_at": "2026-10-17T14:02:11Z",
  "monitor": {
    "id": 12,
    "name": "Shop",
    "url": "https://shop.example.com",
    "history_url": "https://cronengine.com/uptime/history?id=12",
    "state": "down"
  },
  "check": {
    "http_status": 502,
    "error": null,
    "confirmations": 3,
    "checked_at": "2026-10-17T14:02:11Z"
  }
}
Status page

Put your status page on your own address.

Every status page works straight away at cronengine.com/status/your-name. On Pro and up you can move it to an address of your own, such as status.yourcompany.com.

  1. 1Type the address in the status page settings.
  2. 2Add the DNS record shown there at your domain provider.
  3. 3Wait for it to be picked up. We check it and set up the certificate for you.
  4. 4Link to it from your site, your help pages and your error pages.

Use a subdomain. A bare domain such as yourcompany.com cannot carry this kind of record at most providers.

DNS record

TypeNamePoints to
CNAMEstatuspages.cronengine.com

Then link to it

<a href="https://status.yourcompany.com">System status</a>

Ready to connect one?

Pick a recipe and try it in two minutes.

Every job and monitor has a test button, so you see the first request or ping arrive before you rely on it.