~/docs / mercob
Mercob
The scheduled-job engine for the Mercy platform. Mercob polls the database for due jobs, dispatches them onto the same BullMQ queue used by Mercio, and tracks every execution attempt from dispatch to completion.
Overview
Mercob sits on top of Mercio. It does not run function code itself — it schedules HTTP calls to a deployed Mercio function and records the results. Every scheduled job:
- ›Targets a deployed Mercio function by its ID.
- ›Supports five schedule kinds: CRON, DAILY, WEEKLY, INTERVAL, and ONCE.
- ›Configures the exact HTTP request to send (method, path, query, headers, body).
- ›Retries on failure up to a configured limit, with per-attempt status tracking.
- ›Enforces a per-invocation timeout. Timed-out jobs are marked TIMEOUT, not lost.
- ›Records a full JobRun for every execution attempt, with logs streamed from mercio-runtime.
How it works
- 01
Poller tick (every 60 s)
The mercob service queries ScheduledJob rows where active = true and nextRunAt ≤ now + 60 s. The 60-second look-ahead window prevents jobs from being skipped due to clock skew. Due jobs are dispatched in parallel, up to 10 at a time.
- 02
Dispatch
For each due job, mercob creates a JobRun row (status = QUEUED) and calls the dispatch loop. Dispatch pushes a BullMQ job onto the mercio-invocations queue with the configured HTTP request parameters and timeoutMs.
- 03
Execution via mercio-runtime
mercio-runtime picks up the job, routes it to the warm workerd process for the target function ID, and makes the HTTP request. It enforces timeoutMs via AbortController. On completion (success or timeout), the BullMQ job result or failure is recorded.
- 04
Result & retry
Dispatch reads the BullMQ result. On success, the JobRun is patched to SUCCEEDED with HTTP status, response body, and duration. On failure, dispatch retries up to maxRetries times. After all retries are exhausted, the run is marked FAILED or TIMEOUT.
- 05
Advance schedule
After the run completes (regardless of outcome), nextRunAt is advanced for recurring jobs. One-shot jobs (ONCE) are deactivated after firing.
Quickstart
Before creating a scheduled job you need a deployed Mercio function. See the Mercio quickstart.
Create a scheduled job via the dashboard
- 01Navigate to /dashboard/mercob and click "New job".
- 02Select the target Mercio function from the dropdown.
- 03Give the job a name and configure the schedule (see Schedule kinds below).
- 04Set the HTTP method, path, and optional query/headers/body.
- 05Configure maxRetries (default 0) and timeoutMs (default 30 000 ms).
- 06Save. The job is active immediately and will fire on its next scheduled time.
Create a job via the API
TOKEN="<your-jwt>"
FUNCTION_ID="<mercio-function-id>"
curl -X POST https://api.yourdomain.com/api/mercob/jobs \
-H "Authorization: Bearer $TOKEN" \
-H "content-type: application/json" \
-d '{
"functionId": "'$FUNCTION_ID'",
"name": "daily-report",
"scheduleKind": "DAILY",
"timeOfDay": "09:00",
"method": "GET",
"path": "/",
"maxRetries": 2,
"timeoutMs": 15000
}'Manually trigger a job
JOB_ID="<job-id>"
curl -X POST https://api.yourdomain.com/api/mercob/jobs/$JOB_ID/trigger \
-H "Authorization: Bearer $TOKEN"Triggering manually creates a JobRun and dispatches it immediately, regardless of the schedule.
Schedule kinds
Set scheduleKind to one of the following values.
| Kind | Required fields | Description |
|---|---|---|
INTERVAL | intervalSec | Runs every N seconds after the previous run completes. |
DAILY | timeOfDay (HH:MM UTC) | Runs once per day at the specified UTC time. |
WEEKLY | timeOfDay, daysOfWeek (0=Sun) | Runs on specified days of the week at the given UTC time. |
CRON | cronExpr | Standard 5-field cron expression (parsed with cron-parser). Times are UTC. |
ONCE | runAt (ISO datetime) | Fires once at the given time, then deactivates itself. |
Examples
// Every 5 minutes
{ "scheduleKind": "INTERVAL", "intervalSec": 300 }
// Every day at 8:30 AM UTC
{ "scheduleKind": "DAILY", "timeOfDay": "08:30" }
// Every Monday and Wednesday at noon UTC
{ "scheduleKind": "WEEKLY", "timeOfDay": "12:00", "daysOfWeek": [1, 3] }
// Standard cron — every hour on weekdays
{ "scheduleKind": "CRON", "cronExpr": "0 * * * 1-5" }
// One-shot at a specific time
{ "scheduleKind": "ONCE", "runAt": "2026-06-01T09:00:00.000Z" }Retries & timeouts
| Field | Default | Description |
|---|---|---|
maxRetries | 0 | Number of retry attempts after the first failure. 0 = no retries, run fails immediately. |
timeoutMs | 30000 | Per-attempt timeout in milliseconds. Enforced by AbortController inside mercio-runtime. |
Mercob waits for timeoutMs + 30 000 ms before giving up on a BullMQ job. The extra 30-second buffer ensures that mercio-runtime has time to cleanly fail the job before the dispatcher times out — preventing duplicate runs.
Run history
Every dispatch creates a JobRun record. You can view run history in the dashboard at /dashboard/mercob/[id] or via the API.
Run statuses
| Status | Meaning |
|---|---|
QUEUED | JobRun created; BullMQ job not yet picked up. |
RUNNING | mercio-runtime is executing the function. |
SUCCEEDED | Function returned a response within timeoutMs. |
FAILED | All retry attempts exhausted with non-successful outcomes. |
TIMEOUT | Function did not respond within timeoutMs on the final attempt. |
Fetch run history via the API
JOB_ID="<job-id>"
# paginated run list
curl -s -H "Authorization: Bearer $TOKEN" \
"https://api.yourdomain.com/api/mercob/jobs/$JOB_ID/runs?page=1&limit=20"
# single run with logs
RUN_ID="<run-id>"
curl -s -H "Authorization: Bearer $TOKEN" \
"https://api.yourdomain.com/api/mercob/runs/$RUN_ID"Configuration
Set these in apps/mercob/.env.
| Variable | Required | Description |
|---|---|---|
DATABASE_URL | yes | PostgreSQL connection string |
REDIS_HOST | yes | Redis host |
REDIS_PORT | no | Redis port (default 6379) |
WORKER_SECRET | yes | Shared secret for internal API calls (PATCH /internal/job-runs, POST /internal/job-logs) |
API_BASE_URL | no | Base URL of the API service (default http://localhost:3001) |