APIs and scheduled jobs

Not everything is a website. An API or backend service gets uptime checks and, if you want, its own numbers; a job that runs on a schedule pings Mosaicdeck after each run. Both sit next to your websites in the portfolio, the alerts and your AI connection.

APIs and backend services

Add the API's address like any other. When it answers with JSON, or with an error page and a health check behind it,Mosaicdeck adds it as an API: no tracking script, and uptime checks on its health check if one answers at/health, /healthz or /api/health (with anything but an HTML page). Otherwise uptime checks the address itself, and the checkup suggests adding a health check. If an API was added as a website, change its kind to API or backend service in its Settings.

A good health check is cheap, needs no authentication, returns no secrets, and answers 200 only when the service can do its job (for example, when its database answers):

// GET /health: 200 when the service and what it depends on are fine, 503 when not. Cheap, no secrets, no auth.
app.get("/health", async (_req, res) => {
  const db = await pingDatabase().then(() => "ok", () => "down");
  res.status(db === "ok" ? 200 : 503).json({ status: db === "ok" ? "ok" : "down", checks: { database: db } });
});

For the API's own numbers (requests, queue depth, error rate, each dependency's status), add aMetrics Protocol endpoint. The project's Set up with an AI agent prompt asks a coding agent to do both.

Scheduled jobs

For a cron job, backup, queue worker or anything else without an address, choose No address? Add a scheduled job on Add project. Give it a name and say how often it runs (every 5 minutesup to once a week). You get a ping URL once: store it as MOSAICDECK_HEARTBEAT_URL wherever the job runs, as a secret.

RequestMeans
GET or POST the URLA run finished successfully.
…/startA run is starting (optional). The next ping then records how long it took.
POST …/failA run failed. The body is the reason: plain text, up to 1,000 characters, kept on the run (alerts show its start).

A run that fails opens an incident and alerts you at once. So does a run that doesn't arrive: after the interval plus a grace period (by default a quarter of the interval to the minute, between 5 minutes and 6 hours; for a daily job, 6 hours). The next successful ping resolves it. Missed runs are only checked after the first ping, so a job you haven't wired up yet never alerts. Change the schedule, pause monitoring (an open incident ends, without a "resolved" alert) or make a new ping URL on the job's Connections tab; a new URL stops the old one counting at once (for up to a minute it can still answer, but nothing it sends is recorded).

Shell or cron

# After the work. A ping that fails never fails the job.
if ./run-job.sh; then
  curl -fsS -m 10 --retry 1 "$MOSAICDECK_HEARTBEAT_URL" > /dev/null || true
else
  curl -fsS -m 10 --retry 1 --data-raw "run-job.sh failed" "$MOSAICDECK_HEARTBEAT_URL/fail" > /dev/null || true
fi

Node or Bun

async function ping(suffix = "", reason) {
  const url = process.env.MOSAICDECK_HEARTBEAT_URL;
  if (!url) return console.warn("heartbeat URL not set; skipping the ping");
  await fetch(url + suffix, { method: reason ? "POST" : "GET", body: reason?.slice(0, 1000), signal: AbortSignal.timeout(10_000) }).catch(() => {});
}

await ping("/start"); // optional: times the run
try {
  await runJob();
  await ping();
} catch (err) {
  await ping("/fail", String(err));
  throw err;
}

Cloudflare Worker

A Worker reads the URL from env, not process.env.

// A Cron Trigger. Store the URL with: wrangler secret put MOSAICDECK_HEARTBEAT_URL
export default {
  async scheduled(controller, env, ctx) {
    const ping = (suffix = "", reason) => env.MOSAICDECK_HEARTBEAT_URL
      ? fetch(env.MOSAICDECK_HEARTBEAT_URL + suffix, { method: reason ? "POST" : "GET", body: reason?.slice(0, 1000), signal: AbortSignal.timeout(10_000) }).catch(() => {})
      : Promise.resolve();
    await ping("/start"); // optional: times the run
    try {
      await runJob(env);
      await ping();
    } catch (err) {
      await ping("/fail", String(err));
      throw err;
    }
  },
};

GitHub Actions

# Add the URL as a repository secret named MOSAICDECK_HEARTBEAT_URL, then end the job with:
- name: Report the run
  if: always()
  run: curl -fsS -m 10 --retry 1 "${{ secrets.MOSAICDECK_HEARTBEAT_URL }}${{ job.status != 'success' && '/fail' || '' }}" > /dev/null || true

Pings never count toward your monthly events. Anyone with the ping URL can report runs, so keep it out of code, logs and chat, and keep personal data and secrets out of failure reasons.

The checkup

Each project's Connections tab has a short checkup of good practice for its kind. Anything missing shows as a gentle suggestion with why it matters and how to do it; nothing is required, and you can dismiss what doesn't apply. Websites and APIs are checked when they're added and whenever you choose Check again; a job's items come from its pings.

Websites

  • Use HTTPS. Traffic travels encrypted, browsers stop marking pages as not secure, and uptime checks and the tracking script need it.
  • Send a Strict-Transport-Security header. It tells browsers to always use HTTPS for the site, so no visit starts over plain HTTP.
  • Set a Content-Security-Policy. It limits which scripts can run on your pages, which blunts injected or compromised scripts.
  • Send X-Content-Type-Options: nosniff. It stops browsers from guessing file types, a common way to slip a script in as something else.
  • Give the homepage a title. The title shows in browser tabs, bookmarks, search results and link previews.
  • Add a meta description. Search engines and link previews show it under the title, so people know what the page is before they click.
  • Make the page mobile-friendly. Without a viewport tag, phones show a zoomed-out desktop page that people have to pinch to read.

APIs

  • Use HTTPS. Traffic travels encrypted, browsers stop marking pages as not secure, and uptime checks and the tracking script need it.
  • Add a health check. An API's base URL often answers 404 or 401 even when it's fine, so uptime can't tell working from broken. A health check answers 200 only when the service and its main dependencies work.
  • Point uptime at the health check. Checking the health endpoint instead of the base URL means a broken dependency shows as down.
  • Report the API's own numbers. A metrics endpoint shows requests, queue depth or error counts next to uptime, and lets you alert on them.
  • Report each dependency as a status metric. A status metric for the database or a queue lets Mosaicdeck alert you when a dependency fails, even while the API still answers.

Scheduled jobs

  • Ping when the job runs. Without pings Mosaicdeck can't tell whether the job ran, so a stopped job goes unnoticed.
  • Send a start ping too. Pinging when a run starts shows how long each run takes, and makes a run that hangs easy to spot.
  • Match the schedule to the job. If the schedule is shorter than the real gap between runs, every run looks missed. If it's much longer, a stopped job takes longer to notice.

Open suggestions are also offered, as optional work, in the project's agent prompt.