> ## Documentation Index
> Fetch the complete documentation index at: https://university.triggr-app.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Example: build an uptime monitor

> Check websites and portals every five minutes, email your service desk when one goes down or recovers, and show their status in an internal app.

Check a list of URLs every five minutes, email your service desk when one goes down or recovers, and show every service's status on an internal app page. It's a trimmed-down version of the uptime monitor in our own test organisation.

<Frame caption="Build an uptime monitor: the database, the four workflows, the Status page, the live app and the schedule (1:25).">
  <video controls preload="none" playsInline className="w-full aspect-video rounded-xl" poster="/images/videos/studio-uptime-monitor.jpg" src="https://mintcdn.com/triggr/GxrCQFDFNLmEMyJ1/images/videos/studio-uptime-monitor.mp4?fit=max&auto=format&n=GxrCQFDFNLmEMyJ1&q=85&s=0820fe196c90f90cc53fed0de6175ae5" data-path="images/videos/studio-uptime-monitor.mp4" />
</Frame>

<Frame caption="The full uptime monitor in our test organisation. The page you build here shows the same services in a simpler table.">
  <img src="https://mintcdn.com/triggr/XztmSfsoAUpshfIO/images/studio/internal-apps/uptime-monitor-status.png?fit=max&auto=format&n=XztmSfsoAUpshfIO&q=85&s=4b3cac7d04035f4dd99205f2ca992eba" alt="An uptime Status page reading All systems operational, with tiles for 4 services, 4 operational, 0 down and 99.97% uptime over 30 days, and a Services list showing Client portal, Status page, Remote access gateway and Ticket portal with their example.com-style URLs, a 30-day bar of daily results, uptime and average response time, each marked Operational." width="2880" height="1895" data-path="images/studio/internal-apps/uptime-monitor-status.png" />
</Frame>

You need a PostgreSQL database that Triggr can reach over the internet, with [Triggr's IP addresses](/security/ip-allowlist) allowed through its firewall.

## How it works

```mermaid theme={null}
flowchart TD
  cron(["Cron, every 5 minutes"]) --> dispatch["Uptime - Dispatch Checks"]
  button(["Check now on the page"]) -- "triggr.runWorkflow" --> dispatch
  dispatch -- "one run per monitor" --> check["Uptime - Check Monitor"]
  check -- "waits for" --> probe["Uptime - Probe Endpoint"]
  probe -- "GET" --> site(("Monitored URL"))
  check -- "saves the result" --> db[("Your PostgreSQL database")]
  check -- "went down or recovered" --> email(["Email to your service desk"])
  page(["The page loads"]) -- "webhook with ?await=true" --> status["Uptime - Status"]
  status -- "reads" --> db
```

Dispatch Checks reads the monitors from your database and starts Check Monitor for each one without waiting. Check Monitor runs Probe Endpoint and waits for its answer: a site that doesn't answer at all fails only the probe's run, recorded as No response. It then saves the result, marks the monitor down after two failures in a row and emails when it goes down or comes back. The page can't read your database, so it calls Status through its webhook, and its Check now button starts a sweep with [`triggr.runWorkflow`](/studio/internal-apps/sdk#run-a-workflow).

## Build it

<Steps>
  <Step title="Create the tables">
    In your database's own SQL editor, run this, with one `uptime_monitors` row per site to watch.

    ```sql theme={null}
    CREATE TABLE uptime_monitors (
      id              serial PRIMARY KEY,
      name            text NOT NULL,
      url             text NOT NULL,
      status          text NOT NULL DEFAULT 'unknown',
      failures        int  NOT NULL DEFAULT 0,
      last_checked_at timestamptz,
      last_error      text NOT NULL DEFAULT ''
    );

    CREATE TABLE uptime_checks (
      monitor_id int NOT NULL REFERENCES uptime_monitors (id) ON DELETE CASCADE,
      checked_at timestamptz NOT NULL DEFAULT now(),
      ok         boolean NOT NULL
    );
    CREATE INDEX ON uptime_checks (monitor_id, checked_at);

    INSERT INTO uptime_monitors (name, url) VALUES
      ('Client portal', 'https://www.example.com'),
      ('Status page',   'https://status.example.org');
    ```
  </Step>

  <Step title="Connect the database">
    In Data > Databases, click **New Connection > Connection String**, paste your connection string and click Create Connection, then click Test Connection on its row (see [Databases](/studio/data/databases)).
  </Step>

  <Step title="Import Uptime - Probe Endpoint">
    In Workflows, click **Import Workflow**, paste this and click Import Workflow. It opens in the designer: click Save.

    <Frame>
      <img src="https://mintcdn.com/triggr/_d7ojda9vf8IHE31/images/studio/internal-apps/import-workflow.png?fit=max&auto=format&n=_d7ojda9vf8IHE31&q=85&s=952a0b33d47bc4a6a8a38a7f02767ff9" width="672" alt="The Import Workflow dialog with Paste JSON and Upload File tabs, an empty box for a workflow execution definition JSON, and Cancel and Import Workflow buttons." data-path="images/studio/internal-apps/import-workflow.png" />
    </Frame>

    ```json Uptime - Probe Endpoint theme={null}
    {
      "id": "set-on-import",
      "name": "Uptime - Probe Endpoint",
      "steps": [
        {"id": "trigger-1", "type": "trigger", "label": "Trigger", "data": {"triggers": []}, "next": ["request"]},
        {"id": "request", "type": "http_request", "label": "Request", "data": {"method": "GET", "url": "{{ ctx.params.url }}", "bodyType": "none", "allowNon2xxStatus": true, "allowInsecureTLS": false}, "next": ["finish"]},
        {"id": "finish", "type": "finish_workflow", "label": "Finish Workflow", "data": {"outcome": "success", "comment": "{{ ctx.steps['Request'].status }}"}, "next": []}
      ],
      "triggers": [],
      "metadata": { "rootNodes": ["trigger-1"] }
    }
    ```

    Click Request to see its settings: Allow Non-2xx Status is on, so an error page still counts as an answer, and Finish Workflow returns the HTTP status as its Comment.

    <Frame>
      <img src="https://mintcdn.com/triggr/XztmSfsoAUpshfIO/images/studio/internal-apps/probe-request-step.png?fit=max&auto=format&n=XztmSfsoAUpshfIO&q=85&s=62faefd22b18f0bd438f04f507093799" width="384" alt="The Request step: Method GET, URL set to ctx.params.url, no headers, Body Type None, Allow Non-2xx Status ticked and Allow Insecure TLS unticked." data-path="images/studio/internal-apps/probe-request-step.png" />
    </Frame>
  </Step>

  <Step title="Import Uptime - Check Monitor">
    Import this the same way.

    ```json Uptime - Check Monitor theme={null}
    {
      "id": "set-on-import",
      "name": "Uptime - Check Monitor",
      "steps": [
        {"id": "trigger-1", "type": "trigger", "label": "Trigger", "data": {"triggers": []}, "next": ["probe"]},
        {"id": "probe", "type": "trigger_workflow", "label": "Probe", "data": {"workflow": "", "awaitCompletion": true, "params": "{ \"url\": {{ ctx.params.url | json }} }"}, "next": ["evaluate"]},
        {"id": "evaluate", "type": "code", "label": "Evaluate", "data": {"javascript": "// A failed probe run means no response: DNS, refused, TLS error or the 30-second timeout.\nvar m = ctx.params;\nvar probe = ctx.steps['Probe'];\nvar code = probe.status === 'completed' ? Number(probe.finishNodeOutput.comment) : 0;\nvar ok = code >= 200 && code < 400;\n\n// Two failures in a row before it counts as down, so one blip doesn't page anyone.\nvar failures = ok ? 0 : Number(m.failures) + 1;\nvar status = ok ? 'up' : (failures >= 2 ? 'down' : m.status);\nvar error = ok ? '' : (code ? 'HTTP ' + code : 'No response');\n\nvar alert = 'none';\nif (status === 'down' && m.status !== 'down') alert = 'down';\nif (status === 'up' && m.status === 'down') alert = 'up';\n\nfunction q(s) { return \"'\" + String(s).replace(/'/g, \"''\") + \"'\"; }\nvar id = parseInt(m.id, 10);\n\nreturn {\n  alert: alert,\n  error: error,\n  sql: 'WITH c AS (INSERT INTO uptime_checks (monitor_id, ok) VALUES (' + id + ', ' + ok + ')) ' +\n       'UPDATE uptime_monitors SET status = ' + q(status) + ', failures = ' + failures +\n       ', last_checked_at = now(), last_error = ' + q(error) + ' WHERE id = ' + id\n};"}, "next": ["save-result"]},
        {"id": "save-result", "type": "execute_sql", "label": "Save Result", "data": {"database": "", "sql": "{{ ctx.steps['Evaluate'].sql }}"}, "next": ["needs-alert"]},
        {"id": "needs-alert", "type": "condition", "label": "Needs Alert", "data": {"conditionLanguage": "javascript", "conditions": [{"id": "down", "label": "Went down", "condition": "ctx.steps['Evaluate'].alert === 'down'"}, {"id": "up", "label": "Recovered", "condition": "ctx.steps['Evaluate'].alert === 'up'"}], "defaultLabel": "No change"}, "next": ["down-alert:down", "recovery-alert:up", "finish:default"]},
        {"id": "down-alert", "type": "send_email", "label": "Down Alert", "data": {"to": "servicedesk@yourmsp.com", "subject": "DOWN: {{ ctx.params.name }}", "body": "{{ ctx.params.url }} has failed two checks in a row: {{ ctx.steps['Evaluate'].error }}."}, "next": ["finish"]},
        {"id": "recovery-alert", "type": "send_email", "label": "Recovery Alert", "data": {"to": "servicedesk@yourmsp.com", "subject": "RECOVERED: {{ ctx.params.name }}", "body": "{{ ctx.params.url }} is responding again."}, "next": ["finish"]},
        {"id": "finish", "type": "finish_workflow", "label": "Finish Workflow", "data": {"outcome": "success", "comment": ""}, "next": []}
      ],
      "triggers": [],
      "metadata": { "rootNodes": ["trigger-1"] }
    }
    ```

    <Frame caption="Uptime - Check Monitor after import.">
      <img src="https://mintcdn.com/triggr/_d7ojda9vf8IHE31/images/studio/internal-apps/check-monitor-canvas.png?fit=max&auto=format&n=_d7ojda9vf8IHE31&q=85&s=a22eb83cf6dc03168c3a3f24cebe3195" width="500" alt="A workflow canvas: Trigger, Probe, Evaluate and Save Result in a line, then Needs Alert branching to Down Alert, Recovery Alert and straight to Finish Workflow." data-path="images/studio/internal-apps/check-monitor-canvas.png" />
    </Frame>

    Click **Probe** and choose Uptime - Probe Endpoint as its Workflow, click Save Result and choose your database, change To on Down Alert and Recovery Alert to your service desk's address, then click Save.

    <Frame>
      <img src="https://mintcdn.com/triggr/_d7ojda9vf8IHE31/images/studio/internal-apps/check-probe-step.png?fit=max&auto=format&n=_d7ojda9vf8IHE31&q=85&s=27c1260e9729f949e111318f299936e9" width="384" alt="The Probe step: Workflow Uptime - Probe Endpoint, Await Completion ticked, and Parameters passing url from ctx.params.url with the json filter." data-path="images/studio/internal-apps/check-probe-step.png" />
    </Frame>

    Evaluate counts a status from 200 to 399 as up and the second failure in a row as down; Needs Alert only emails on a change. To alert on the first failure, change `failures >= 2` to `failures >= 1` in Evaluate.
  </Step>

  <Step title="Import Uptime - Dispatch Checks">
    Import this.

    ```json Uptime - Dispatch Checks theme={null}
    {
      "id": "set-on-import",
      "name": "Uptime - Dispatch Checks",
      "steps": [
        {"id": "trigger-1", "type": "trigger", "label": "Trigger", "data": {"triggers": []}, "next": ["find-monitors"]},
        {"id": "find-monitors", "type": "execute_sql", "label": "Find Monitors", "data": {"database": "", "sql": "SELECT id, name, url, status, failures FROM uptime_monitors ORDER BY id"}, "next": ["each-monitor"]},
        {"id": "each-monitor", "type": "for_loop", "label": "Each Monitor", "data": {"expression": "ctx.steps['Find Monitors'].data", "variableName": "monitor"}, "next": ["check:body", "finish:complete"]},
        {"id": "check", "type": "trigger_workflow", "label": "Check", "data": {"workflow": "", "awaitCompletion": false, "params": "{{ ctx.monitor | json }}"}, "next": ["end-loop"]},
        {"id": "end-loop", "type": "end_for_loop", "label": "End For Loop", "data": {}, "next": []},
        {"id": "finish", "type": "finish_workflow", "label": "Finish Workflow", "data": {"outcome": "success", "comment": ""}, "next": []}
      ],
      "triggers": [],
      "metadata": { "rootNodes": ["trigger-1"] }
    }
    ```

    <Frame caption="Uptime - Dispatch Checks after import.">
      <img src="https://mintcdn.com/triggr/_d7ojda9vf8IHE31/images/studio/internal-apps/dispatch-canvas.png?fit=max&auto=format&n=_d7ojda9vf8IHE31&q=85&s=73d57dd64f2d84d62ad7f750077d013f" width="440" alt="A workflow canvas: Trigger, Find Monitors, then an Each Monitor loop whose body runs Check and End For Loop, and whose completion leads to Finish Workflow." data-path="images/studio/internal-apps/dispatch-canvas.png" />
    </Frame>

    Click **Find Monitors** and choose your database, click Check and choose Uptime - Check Monitor as its Workflow, then click Save. You'll add its schedule once you've tested it.

    <Frame>
      <img src="https://mintcdn.com/triggr/_d7ojda9vf8IHE31/images/studio/internal-apps/dispatch-find-monitors-step.png?fit=max&auto=format&n=_d7ojda9vf8IHE31&q=85&s=0a38a1c704582c9c6794b1ff869fb973" width="384" alt="The Find Monitors Execute SQL step: Database MSP data and a SQL Query selecting id, name, url, status and failures from uptime_monitors ordered by id." data-path="images/studio/internal-apps/dispatch-find-monitors-step.png" />
    </Frame>
  </Step>

  <Step title="Import Uptime - Status">
    Import this, click **Load Status**, choose your database and click Save. Its Trigger already holds a Webhook trigger.

    ```json Uptime - Status theme={null}
    {
      "id": "set-on-import",
      "name": "Uptime - Status",
      "steps": [
        {"id": "trigger-1", "type": "trigger", "label": "Trigger", "data": {"triggers": [{"id": "trigger-webhook", "type": "webhook", "formId": "", "chron": "", "webhookEndpoint": null}]}, "next": ["load-status"]},
        {"id": "load-status", "type": "execute_sql", "label": "Load Status", "data": {"database": "", "sql": "SELECT COALESCE(json_agg(json_build_object(\n  'name', m.name,\n  'status', m.status,\n  'lastCheckedAt', m.last_checked_at,\n  'lastError', m.last_error,\n  'uptime', (SELECT round(100.0 * count(*) FILTER (WHERE c.ok) / NULLIF(count(*), 0), 2)\n             FROM uptime_checks c\n             WHERE c.monitor_id = m.id AND c.checked_at > now() - interval '30 days')\n) ORDER BY m.name), '[]')::text AS monitors\nFROM uptime_monitors m"}, "next": ["finish"]},
        {"id": "finish", "type": "finish_workflow", "label": "Finish Workflow", "data": {"outcome": "success", "comment": "{{ ctx.steps['Load Status'].data[0].monitors }}"}, "next": []}
      ],
      "triggers": [{"id": "trigger-webhook", "type": "webhook", "formId": "", "chron": "", "webhookEndpoint": null}],
      "metadata": { "rootNodes": ["trigger-1"] }
    }
    ```

    It returns each monitor's name, status, 30-day uptime, last check and last error as JSON, without the URLs: anyone with a webhook's URL can run it and read what it returns.
  </Step>

  <Step title="Add the app page">
    In Internal Apps, click **New App**, name it `Uptime` and click Create App. Click Design on its row, click Add Page, name it `Status` and click Create Page.

    Paste this page and replace `STATUS_WORKFLOW_ID` and `DISPATCH_WORKFLOW_ID` with the IDs of Uptime - Status and Uptime - Dispatch Checks, from each workflow's address in Studio (`app.triggr-app.com/workflows/<id>/design`). Click Save, then **Preview**.

    ```html Status page theme={null}
    <!doctype html>
    <html>
    <head>
    <style>
      body { margin: 0; padding: 24px; background: #09090b; color: #fafafa; font: 14px -apple-system, "Segoe UI", Roboto, sans-serif; }
      header { display: flex; align-items: center; gap: 12px; margin-bottom: 16px; }
      h1 { font-size: 18px; margin: 0; flex: 1; }
      button { background: #6366f1; color: #fff; border: 0; border-radius: 8px; padding: 8px 16px; font: inherit; cursor: pointer; }
      button:disabled { opacity: .5; }
      #summary { padding: 16px; border-radius: 10px; margin-bottom: 16px; font-weight: 600; }
      .ok { background: rgba(34,197,94,.15); color: #22c55e; }
      .bad { background: rgba(239,68,68,.15); color: #ef4444; }
      table { width: 100%; border-collapse: collapse; background: #18181b; border-radius: 10px; }
      th, td { text-align: left; padding: 10px 14px; border-bottom: 1px solid #27272a; }
      th { color: #a1a1aa; font-weight: 500; }
      .pill { padding: 2px 10px; border-radius: 20px; font-size: 12px; font-weight: 600; background: #27272a; color: #a1a1aa; }
      .pill.up { background: rgba(34,197,94,.15); color: #22c55e; }
      .pill.down { background: rgba(239,68,68,.15); color: #ef4444; }
      #message { color: #a1a1aa; margin-top: 12px; }
    </style>
    </head>
    <body>
      <header>
        <h1>Uptime</h1>
        <button id="check">Check now</button>
      </header>
      <div id="summary">Loading…</div>
      <table>
        <thead><tr><th>Service</th><th>Status</th><th>Uptime (30 days)</th><th>Last checked</th><th>Last error</th></tr></thead>
        <tbody id="rows"></tbody>
      </table>
      <p id="message"></p>

    <script>
      // Replace these with your workflows' IDs (from the address bar in the designer).
      var STATUS_URL = 'https://api.triggr-app.com/telescope/webhook/STATUS_WORKFLOW_ID?await=true&timeout=30';
      var DISPATCH_WORKFLOW_ID = 'DISPATCH_WORKFLOW_ID';

      function esc(s) {
        return String(s == null ? '' : s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
      }

      // Runs Uptime - Status through its webhook and returns the monitors it lists.
      async function loadMonitors() {
        var res = await fetch(STATUS_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' });
        if (!res.ok) throw new Error('Status request failed (HTTP ' + res.status + ')');
        var run = await res.json();
        if (run.status !== 'completed') throw new Error('Uptime - Status ' + run.status + ': ' + (run.error || ''));
        return JSON.parse(run.output.comment);
      }

      async function refresh() {
        var summary = document.getElementById('summary');
        try {
          var monitors = await loadMonitors();
          var down = monitors.filter(function (m) { return m.status === 'down'; });
          summary.className = down.length ? 'bad' : 'ok';
          summary.textContent = down.length
            ? 'Down: ' + down.map(function (m) { return m.name; }).join(', ')
            : 'All systems operational';
          document.getElementById('rows').innerHTML = monitors.map(function (m) {
            return '<tr><td>' + esc(m.name) + '</td>' +
              '<td><span class="pill ' + esc(m.status) + '">' + esc(m.status) + '</span></td>' +
              '<td>' + (m.uptime == null ? '-' : esc(m.uptime) + '%') + '</td>' +
              '<td>' + (m.lastCheckedAt ? new Date(m.lastCheckedAt).toLocaleString() : 'Never') + '</td>' +
              '<td>' + esc(m.lastError) + '</td></tr>';
          }).join('');
        } catch (err) {
          summary.className = 'bad';
          summary.textContent = err.message;
        }
      }

      // Starts a sweep now. The results arrive about half a minute later.
      document.getElementById('check').addEventListener('click', async function () {
        var button = this;
        var message = document.getElementById('message');
        button.disabled = true;
        try {
          await triggr.runWorkflow(DISPATCH_WORKFLOW_ID, {});
          message.textContent = 'Checking every monitor. This page refreshes in 30 seconds.';
          setTimeout(function () { refresh(); message.textContent = ''; button.disabled = false; }, 30000);
        } catch (err) {
          message.textContent = err.message;
          button.disabled = false;
        }
      });

      refresh();
      setInterval(refresh, 300000); // every 5 minutes, the same as the sweep
    </script>
    </body>
    </html>
    ```

    <Frame caption="Preview loads the table from Uptime - Status.">
      <img src="https://mintcdn.com/triggr/XztmSfsoAUpshfIO/images/studio/internal-apps/uptime-designer-preview.png?fit=max&auto=format&n=XztmSfsoAUpshfIO&q=85&s=ca742ddfefba79226f51bcf5621714f6" alt="The Uptime app editor with the Status page's HTML on the left and a preview on the right: an Uptime heading with a Check now button, a red banner reading Down: Ticket portal, and a table of Client portal, Remote access gateway, Status page and Ticket portal with status pills, 30-day uptime, last checked time and the last error HTTP 503." width="2880" height="1800" data-path="images/studio/internal-apps/uptime-designer-preview.png" />
    </Frame>
  </Step>

  <Step title="Let the page start sweeps">
    Click **Settings**, leave Authentication Type as Internal, tick Uptime - Dispatch Checks under Workflows pages can run and click Save Settings.

    <Frame>
      <img src="https://mintcdn.com/triggr/_d7ojda9vf8IHE31/images/studio/internal-apps/app-settings-uptime.png?fit=max&auto=format&n=_d7ojda9vf8IHE31&q=85&s=303b361bfc0c70b7c0767f6a75c7a109" width="419" alt="The App Authentication panel with Internal selected, Hide top bar off, an Internal-only note that the app is staff-only, Allow file uploads off, and Uptime - Dispatch Checks ticked under Workflows pages can run above Uptime - Check Monitor, Uptime - Probe Endpoint and Uptime - Status." data-path="images/studio/internal-apps/app-settings-uptime.png" />
    </Frame>

    Your team opens the live app at `https://app.triggr-app.com/telescope/apps/<org-id>/<app-id>` with their staff sign-in (**Support > Copy Org ID** in the top bar gives the first ID; the second is in the designer's address bar). Open on Internal Apps shows the page without the SDK, so Check now only works on that link.
  </Step>
</Steps>

## Test it

<Steps>
  <Step title="Probe a URL">
    Open Uptime - Probe Endpoint, click **Manual Trigger**, enter `{ "url": "https://www.example.com" }` and click Trigger Workflow. In the run, Finish Workflow's output has `comment: "200"`.

    <Frame>
      <img src="https://mintcdn.com/triggr/_d7ojda9vf8IHE31/images/studio/internal-apps/manual-trigger-probe.png?fit=max&auto=format&n=_d7ojda9vf8IHE31&q=85&s=331e628511aa75d86915c0c8f7279cab" width="560" alt="The Trigger Workflow dialog with the JSON url https://www.example.com entered, and Cancel and Trigger Workflow buttons." data-path="images/studio/internal-apps/manual-trigger-probe.png" />
    </Frame>
  </Step>

  <Step title="Check one monitor">
    Open Uptime - Check Monitor and trigger it with the first monitor's row:

    ```json theme={null}
    { "id": "1", "name": "Client portal", "url": "https://www.example.com", "status": "unknown", "failures": "0" }
    ```

    The run reaches Finish Workflow without sending an email, and that monitor's `status` in your database is now `up`.
  </Step>

  <Step title="Run a sweep from the app">
    Open the live app link from the last build step and click Check now. The page refreshes 30 seconds later, and Uptime - Check Monitor's Runs has one run per monitor.
  </Step>

  <Step title="Test an alert">
    Add a monitor that can't answer, click **Check now**, wait for the page to refresh and click it again. The second failure marks Alert test down and emails DOWN: Alert test.

    ```sql theme={null}
    INSERT INTO uptime_monitors (name, url) VALUES ('Alert test', 'https://down.example.invalid');
    ```

    Then delete it: `DELETE FROM uptime_monitors WHERE name = 'Alert test';`
  </Step>
</Steps>

## Go live

Open Uptime - Dispatch Checks, click Trigger, then **Add Trigger**, set Trigger Type to Cron, enter `*/5 * * * *` and click Save.

<Frame>
  <img src="https://mintcdn.com/triggr/_d7ojda9vf8IHE31/images/studio/internal-apps/dispatch-cron-trigger.png?fit=max&auto=format&n=_d7ojda9vf8IHE31&q=85&s=4ee8b5bb71af02d3427b0a2d614f60a5" width="384" alt="The Trigger panel with Trigger 1 expanded: Trigger Type Cron and Cron set to */5 * * * *, with the hint Cron expression for when to trigger the workflow (UTC), and an Add Trigger button." data-path="images/studio/internal-apps/dispatch-cron-trigger.png" />
</Frame>

Check all four workflows show Active on Workflows (imports start Active). Add a row to `uptime_monitors` for each site to watch, and the next sweep picks it up.

## FAQ

<AccordionGroup>
  <Accordion title="Can it check more often than every five minutes?">
    No. [Scheduled runs](/studio/workflows/triggers#cron) are at least five minutes apart.
  </Accordion>

  <Accordion title="Why are there so many runs?">
    Each sweep is one Uptime - Dispatch Checks run plus a check and a probe per monitor (21 runs for 10 monitors), and the page adds an Uptime - Status run each time it loads.
  </Accordion>

  <Accordion title="How do I stop uptime_checks growing forever?">
    Add a workflow with a Cron trigger such as `15 3 * * *` and an Execute SQL step that runs `DELETE FROM uptime_checks WHERE checked_at < now() - interval '90 days'`.
  </Accordion>

  <Accordion title="Can it check sites on a client's internal network?">
    No. Requests come from Triggr's servers, so each URL must be reachable from the internet; a site with an allow list needs [Triggr's IP addresses](/security/ip-allowlist).
  </Accordion>

  <Accordion title="Can I share the page with clients?">
    Not as built: every client would see every monitor. Keep it Internal, or build a page per client with its own Status workflow.
  </Accordion>
</AccordionGroup>

## Troubleshooting

<AccordionGroup>
  <Accordion title="Test Connection fails, and my password has special characters">
    Click **Edit** on the connection and type the password into Password; a connection string needs `@`, `:` and `/` encoded.
  </Accordion>

  <Accordion title="Execute SQL fails with “invalid SQL query”">
    Triggr checks each query with `EXPLAIN` first, so statements such as `CREATE TABLE` fail. Create tables in your database's own SQL editor.
  </Accordion>

  <Accordion title="A run fails with “trigger node … not found” or “no trigger node found”">
    The pasted JSON didn't start with a top-level `"id"`. Import the JSON from this page again.
  </Accordion>

  <Accordion title="The page shows “Status request failed (HTTP 404)” or Check now shows “this app is not allowed to run that workflow”">
    Check the two IDs in the page, that Uptime - Status is saved with its Webhook trigger, and that Uptime - Dispatch Checks is ticked under Workflows pages can run.
  </Accordion>

  <Accordion title="Check now shows “triggr is not defined”">
    You opened the page with Open. Use the live app link from the last build step.
  </Accordion>

  <Accordion title="A monitor stays unknown">
    It hasn't been checked yet, or has failed once and not been up since; its `last_error` column says why.
  </Accordion>
</AccordionGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.