Skip to content

Recording runs

POST /api/v1/runs is the heart of the integration: one HTTP call per execution, from any tool that can speak HTTP.

Request

curl -X POST "$LUMATRACK_URL/api/v1/runs" \
  -H "Authorization: Bearer $LUMATRACK_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "automation": "os-patching",
    "status": "success",
    "executed_at": "2026-06-11T14:30:00Z",
    "duration_seconds": 142,
    "units": 240,
    "source": "ansible",
    "external_id": "tower-job-99412",
    "metadata": {"hosts": 240}
  }'
Field Required Notes
automation yes The automation's slug. GET /api/v1/automations lists them.
status no success (default), failure, skipped or cancelled. A skipped run started and deliberately did no work; a cancelled one was interrupted before it finished. Neither earns value and neither is counted as a failure. See Runs that do no work.
failure_reason no Free-form reason, e.g. auth/credential on a failure, nothing to process on a skip, ci/timeout on a cancellation. Each status groups into its own reason Pareto, and all are filterable on the run list. 200 chars.
executed_at no ISO 8601. Defaults to now. Naive datetimes are treated as UTC. Timestamps more than a few minutes in the future are refused; frozen months are refused.
duration_seconds no Non-negative integer.
units no Records/items this execution processed (patched hosts, reminders sent). Non-negative integer, default 1. Recorded with the run today; per-unit valuation is coming and will multiply value by it.
baseline_minutes_override no The total manual minutes this one execution replaced. Overrides the automation's standard baseline for this run only. Non-negative decimal up to 999999.99; omit it to use the standard baseline. See Asserting a baseline per run.
source no The reporting system, e.g. ansible, n8n, github-actions. Shows up in the run list.
external_id no, but send it Your system's run or job id. Makes ingestion idempotent.
manual no true marks a real manual execution recorded as opportunity evidence (defaults to true on a measuring automation). Manual runs measure what the status quo costs; they never book savings, count toward your event budget, and cannot carry ai or usage.
ai no Token usage for AI runs: {"model", "input_tokens", "output_tokens", "cached_tokens", "cost"?}. LumaTrack prices it server-side; see AI workloads.
metadata no Arbitrary JSON kept with the run (token counts, hosts touched, ticket ids). Capped at 16 KB; store a reference, not a full payload or transcript.

Responses

Code Meaning
201 Recorded.
200 Replay: this external_id already exists, with "deduplicated": true. A full-scope key gets the stored run; an ingest-only key gets an acknowledgement (id, held, external_id) and no stored evidence.
202 Recorded but held: you are over the plan's monthly event cap. The run is stored and excluded from value math until you upgrade or the month resets. The body says so in warning.
400 Something in the body is wrong, or executed_at falls in a frozen month. The error string names the exact field.
401 Missing or invalid API key.
404 No automation with that slug in your organization.
429 Two distinct cases, told apart by the Retry-After header. With Retry-After: the org's per-minute rate limit; back off for that many seconds and retry (see Rate limits). Without it: past 3x the plan's monthly cap; the event was refused (with a durable record kept on our side); held events release when you upgrade.

Held events are released automatically when capacity allows (an upgrade, or the monthly reset). A released event keeps its original executed_at and posts to that month; it counts against the event cap of the month it is released in. If its month has frozen in the meantime, the frozen figures stay as published and the event books a closed-period correction instead: held evidence is never dropped and never re-dated.

Every non-2xx body is {"error": "..."} with a message written for a human.

Practices that pay off

Always send external_id. Delivery code retries; networks flake. With an external_id, a duplicate POST is a no-op instead of double-counted savings. Use the id your platform already has: the n8n execution id, the Ansible Tower job id, the GitHub Actions run id.

Report failures. Wire your error path to send "status": "failure". Failure runs subtract real cost and earn no savings, and the reliability chart needs them to tell the truth. An integration that only reports success produces a number nobody should sign.

Report skips and cancellations too. If your workflow can run and decide there is nothing to do, send "status": "skipped" on that path rather than success; if it can be interrupted part way, send "status": "cancelled". Either one booked as a success invents savings that never happened, and either booked as a failure tells your client an automation is broken when it is merely idle or was stopped.

Let executed_at default for live events. Set it only when reporting something that genuinely happened earlier. If the timestamp falls in a frozen month you get a 400; resubmit without executed_at to land the evidence in the current period, or use backfill for real historical loads.

Put measurements in metadata. Token usage, hosts patched, tickets closed. It is stored with the run and comes back on GET /api/v1/runs.

Asserting a baseline per run

Your automation carries one standard baseline: the manual minutes a typical execution replaces, agreed once and versioned. Some source systems know better than that on a per-execution basis. A ticket platform knows that a password reset historically took 6 minutes and a mailbox migration took 90. When yours does, send the real figure with the run:

curl -X POST "$LUMATRACK_URL/api/v1/runs" \
  -H "Authorization: Bearer $LUMATRACK_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "automation": "ticket-remediation",
    "external_id": "INC-88412",
    "baseline_minutes_override": 45,
    "metadata": {"category": "mailbox-migration"}
  }'

What it replaces. The number is the total manual minutes for that execution: the whole task, start to finish, as a human would have done it. It replaces both parts of the automation's standard baseline, the per-batch minutes and the per-unit minutes, so units on an overridden run stays volume information. It does not replace oversight: the run still nets the review time the automation says it costs, because that is your assumption about the automated world, not the source system's knowledge of the manual one.

How it is priced. At the automation's effective hourly rate (its own rate, else its role's loaded rate, else your organization default), with the same conservatism factor the productivity lane always applies. If the automation decomposes its baseline into steps at several roles, the override prices at the single effective rate: an asserted total does not decompose into steps.

Where it shows up. On the run's detail page, labeled as reported by the source system, and on the per-run statement behind any month's labor figure. LumaTrack did not measure this number and never presents it as though it did.

Send it only where the system genuinely knows. A baseline your ticket platform derives from years of handle-time data is evidence. A number a workflow author typed into a node because it felt about right is the standard baseline with extra steps, and it belongs in the automation's assumptions where it gets an owner, a version history and an attestation date.

0 is a real assertion, meaning this execution replaced no manual work at all, and books $0 for that run. Omit the field entirely to use the standard baseline. A run whose asserted baseline is smaller than its oversight books $0 rather than a negative.

On a failed, skipped or cancelled run the value is stored and returned but earns nothing, exactly like units.

Reading runs back

curl -s "$LUMATRACK_URL/api/v1/runs?automation=os-patching&status=failure&since=2026-06-01T00:00:00Z&limit=50" \
  -H "Authorization: Bearer $LUMATRACK_KEY"

Newest first. Filters: automation (slug), status, since (ISO 8601), limit (default 100, max 500). To page past the window, pass before=<id of the last run you received> (an opaque run_... id) and repeat while the response's has_more field is true. Held runs are included and flagged, and each run carries its metadata.

Runs that do no work

Two statuses cover an execution that produced nothing. Both earn no labor value and neither is ever counted as a failure.

Skipped

A skipped run started and deliberately did no work. The common shapes:

  • an n8n IF/Filter branch short-circuited because the queue was empty
  • a poller found no new tickets
  • a CI job was gated out by a path filter or if: condition
  • a batch job ran inside a maintenance window and stood down

Skipped runs are recorded as evidence. They are real volume, and their absence would make a schedule look quieter than it is. But they are held apart from both other statuses:

Cancelled

A cancelled run was interrupted before it finished:

  • a CI job killed by a timeout, or superseded by a newer push
  • an operator stopping a workflow mid-flight
  • a container evicted, a queue drained during a deploy

The difference from a skip is the money. A skip did no work, so by default it costs nothing. A cancellation was doing work when it died, so by default it is charged.

How the four statuses compare

Labor value Counts as a failure Per-run cost Success rate
success yes no charged numerator
failure no yes charged denominator
skipped no no not charged by default excluded by default
cancelled no no charged by default excluded by default

Both of those are judgement calls, so each automation decides for itself (Automation → Reliability), with a pair of settings per status:

  • Charge the per-run cost? Off for skips, on for cancellations. Turn skips on when starting the workflow costs money regardless: an n8n execution credit, a billed container start. Turn cancellations off when they are queued jobs killed before they ever started.
  • Count against the SLA? Off for both, so success rate is successes over successes plus failures and neither is credit nor blame. Turn either on where that outcome means work that was owed did not happen.

Whatever those say about the FIXED per-run charge, measured spend always books. A run cancelled after burning 40k tokens reports those tokens in its ai block and the ledger charges them: money demonstrably spent is not a policy question.

Shared costs allocated by_volume follow the same basis, so an automation that skips most of the time stops absorbing shared cost as though it ran.

Send a failure_reason to say why: nothing to process or outside business hours on a skip, ci/timeout or ci/superseded on a cancellation. Each status keeps its own Pareto on the automation page, and all are filterable on the run list, which is how "this job skips 90% of the time" turns into "so run it half as often" and a wall of ci/timeout turns into a timeout that wants raising.

Alerts. An automation that quietly does nothing is invisible to a failure chart, so LumaTrack watches skips directly. A skip spike (three quarters or more of the last 24 hours' runs skipped, on at least 10 runs) and a skip streak (10 consecutive skips with nothing in between) each raise an alert and fire the run.skipped_threshold webhook. Both thresholds are configurable per organization in Settings.

A skip spike clears itself once the 24-hour window rolls forward, so it can fire again the next day. A streak does not clear until a non-skipped run lands, so it alerts once and then waits: acknowledge the alert to arm it again, rather than being told the same unchanged fact every night.