API Reference
The workflow engine exposes a REST API on port 3030 (default). The Swagger UI at http://localhost:3030/api, mounted with NODE_ENV=development, is the authoritative, always-up-to-date API reference with request/response schemas and a “Try it out” feature.
Endpoint Groups
| Group | Base Path | Purpose |
|---|---|---|
| Workflow Definitions | /workflow-definition |
Publish, list, delete, and restore workflow definitions (the old create/update routes are removed) |
| Workflow Executions | /workflow-execution |
Trigger, monitor, and abort workflow executions; remediate unsafe failures (/:id/remediation, /:id/remediation/rollback, /:id/remediation/resolve) |
| Blackboard | /blackboard |
Job polling and result submission (used by workers) |
| Function Blocks | /function-blocks |
Registration and lookup of function blocks |
| Workers | /workers |
Worker registration, heartbeat, and lifecycle |
| Health & Version | /health, /health/live, /version |
Probes, version info, and SDK compatibility |
| Schema | /schema |
Workflow JSON Schema (for IDE validation) |
| License | /license |
Active license flavor, quota limits, and live usage counts |
Common Operations
Publish a workflow
curl -X POST http://localhost:3030/workflow-definition/publish \
-H "Content-Type: application/json" \
-d '{ "workflow": { ... } }'
The request body is one DTO: the workflow document under workflow, with the options
(increment, dryRun, acceptBump) beside it. The engine compares the document
against the version it would displace and refuses a declared version that doesn’t bump enough to
honestly describe the change. See Publishing & Versioning for
the full contract – the options, every response status, and the version floor – or
Your First Workflow for a walkthrough.
Removed: POST /workflow-definition and POST /workflow-definition/update
The routes /publish replaces are gone and answer 404. See
The old routes are gone for the
payload mapping if you still have a client on the old shapes.
Delete and restore a workflow definition
curl -X DELETE http://localhost:3030/workflow-definition/wf.example.neops.io%2Fhello_world%3A2.0.0
curl -X POST http://localhost:3030/workflow-definition/wf.example.neops.io%2Fhello_world%3A2.0.0/restore
DELETE soft-deletes: the version stops resolving and disappears from listings, but its
number stays taken and its content is kept. GET .../{id}?findDeleted=true still serves it, with deletedAt set. POST .../{id}/restore clears the tombstone again
(so does republishing the identical document – restore is the way back when you don’t have the
original content at hand). Restoring returns 400 if the definition is not soft-deleted, 404
if it does not exist, and 403 if it would exceed the workflow-definition license cap –
soft-deleting the last live version of a (package, name) frees a licensed slot, so restoring
re-claims one.
Read what a step returned
include=result on GET /blackboard/jobs/page and GET /workflow-execution/{id}/jobs adds resultData to each job row: the data the worker returned for the step. Data larger than a row carries (65,536 characters of JSON text) is left out and the row says resultDataOmitted: true. GET /blackboard/jobs/{jobId}/result serves one job’s status, message and data whole. Both need the workflow-execution:read permission, the same as the job list, and neither redacts anything: the data is what the worker pushed.
Narrow the function block list
GET /function-blocks/registrations/list takes package and name, two optional exact-match filters that combine with each other and with findDeleted. name matches every version of that function block. A repeated parameter answers 400.
Compare two versions of a workflow
curl 'http://localhost:3030/workflow-definition/compare?from=wf.example.neops.io/hello_world:1.0.0&to=wf.example.neops.io/hello_world:2.0.0'
Returns both versions (id, version, createdAt, createdByUsername, deletedAt), the findings between them in words with the step or input each is about, and the floor: the least version bump those changes force, or null when none does. It is the differ publish uses, so the verdict matches what publishing the newer version over the older would say. A soft-deleted version can be compared. Two versions of different workflows answer 400, a missing version 404.
Execute a workflow
curl -X POST http://localhost:3030/workflow-execution \
-H "Content-Type: application/json" \
-d '{
"workflow": "wf.example.neops.io/hello_world:2.0.0",
"executeOnParameters": { "ids": [1, 2, 3] },
"parameters": {}
}'
| Field | Description |
|---|---|
workflow |
SemVer identifier (package/name:version) |
executeOnParameters.ids |
Entity IDs to execute on (from CMS). Omit for global workflows |
parameters |
Runtime parameter values matching the workflow’s parameterSchema |
Page, filter and sort the lists
The lists below have a paged form beside the whole-list route, which is unchanged. Use the paged form for anything a person scrolls or searches.
| Route | Rows |
|---|---|
GET /workflow-execution/page |
Runs, newest first, without their parameters |
GET /blackboard/jobs/page |
Jobs, oldest first |
GET /workflow-definition/page |
Workflow definitions without their documents |
curl 'http://localhost:3030/workflow-execution/page?status=failed_unsafe&status=error&createdAfter=2026-10-01T00:00:00Z&sort=createdAt:desc&limit=50&include=facets'
Every paged route takes limit (0 to 200, default 50; 0 returns the counts alone), cursor,
sort=<field>:<asc|desc>, q and include. Several values of one filter are repeated keys or one
comma-separated value, and filters of different keys combine with and. An unknown query key is
refused with 400. The response holds items, nextCursor (null on the last page), the exact total
that matches the filters, and, when include names them:
facets: counts per value, each under every filter but its own, at mostfacetLimitvalues (default 20), with the values the filter chose always listed. ThefacetQ…keys of each list search inside one facet.histogram: rows per time bucket and status.interval(5m,15m,1h,3h,6h,1d,1wfrom Monday,1mo) sets the bucket width, by default the narrowest that keeps the window within 168 buckets; an interval that makes over 400 buckets is refused.tz(an IANA zone such asEurope/Zurich) cuts the buckets from that zone’s midnight. Bucket edges are instants, so a day is 23 or 25 hours across a clock change and no hour is listed twice.
Pass nextCursor back as cursor with the same filters and sort to read the next page; a cursor
issued under another sort is refused with 400. Rows without a value for the sort field come last in
both directions. changedSince (an ISO time, inclusive) keeps the rows whose updatedAt is at or
after it, and total counts them, for a live refresh that asks again with the newest updatedAt it
saw.
Runs
Runs filter by id (to re-ask rows already loaded), status, workflow (any of: package/name:version
for one version, package/name for every version, or a bare name for every version of workflows called
that), startedBy, triggerType, reasonCode, unsafeCause, createdAfter (inclusive), createdBefore (exclusive), target
(a CMS id in the run’s selection or its resolved context), cmsResourcesLocked and changedSince, and search facets
with facetQWorkflow, facetQStartedBy and facetQTarget. They sort by createdAt and workflow
(the qualified id), the columns the table is indexed on. A run row says whether it still holds its CMS
locks (cmsResourcesLocked).
q matches part of the run id, the workflow name, the reason or the starter, case-insensitively.
Trigram indexes (the pg_trgm extension, created by a migration) serve text of three letters or more;
a search of one or two letters reads the table, so a client waits for three letters.
Jobs
Jobs filter by id, state (pending, polled, then success, success-ack, failed or
failed-ack; canceled for a job the engine closed without a worker, which never ran and says
canceled: true on its row), workflowExecutionId, stepLabel, functionBlockId, type, workerId,
entityId, the same time window and changedSince, and search facets with facetQFunctionBlock, facetQWorker,
facetQStep, facetQEntity and facetQRun. They sort by createdAt, stepLabel and
functionBlockId, the columns the table is indexed on. A job row names its run (workflowExecutionId),
the entity it runs on (entityId, null for a global step), when it was created, claimed by a worker
(startedAt) and finished (finishedAt), its retryCount, the sideEffects its result reported
(null when it reported none), the reasonCode of a failure the engine wrote in its place, resultRejected
when the engine refused the result, and the size of its rollback snapshot (rollbackDataBytes) or why it was
dropped (rollbackDataRejected), never the snapshot. A job’s updatedAt is when it was
claimed, retried or given a result or an acknowledgement. Job facets count the newest hundred thousand
jobs and facets.since says from when; it is null when the facets count every job, as for a list
narrowed to a run or to ids.
Jobs of one run
GET /workflow-execution/<id>/jobs is the job list narrowed to one run, oldest job first, with the
same filters, sort and cursor. The entry from GET /workflow-execution/id/<id> adds the run’s resolved
context and the ids of the locked entities under heldLocks.
Workflow definitions
Workflow definitions list one row per workflow, its highest live version, unless latestOnly=false.
A row carries the description its document declares and its seedEntity; q also matches the
description. They filter by id, package, workflow, seedEntity (also a facet), ranAfter
(an ISO time: rows whose last run was created at or after it), hasRun (rows that have a last run
or have none; the last run is the one lastRun shows, so it follows latestOnly), findDeleted
and deleted (only the soft-deleted versions). id and workflow match the listed rows, so with
latestOnly they find a workflow’s highest version only. With findDeleted and latestOnly, a
workflow’s row is its highest live version, or its highest deleted version when none is live; with
deleted and latestOnly, only the workflows every version of which is deleted, each by its
highest. facetQPackage searches the package facet. They sort by name, package, createdAt
and lastRun. The lastRun of a row says when the run was submitted (createdAt) and how it
started (triggerType). GET /workflow-definition/<id> names the publisher (createdBy,
createdByUsername).
Errors for an unknown id
Every route that names one object (a run, a workflow definition, a function block, a worker) answers 404 with the standard error body when it does not exist, and 400 when the id is not a UUID where a UUID is required. None answers 200 with an empty body.
Health check
curl http://localhost:3030/health/
# {"healthy": true, "authz": {"mode": "enforce", "keySource": "pem", "keyReachable": null}, "cms": {"reachable": true, "compatible": true, "checkedAt": "..."}}
See Operations for what each field means.
Get the workflow schema
Returns the full RootWorkflow JSON Schema. Point your IDE at this endpoint for live validation – see IDE Setup.
Check the license and its quotas
Returns the identity of the license (flavor, displayName, productionUse), and every cap in quotas: for
each its name, a label a person can read, the limit (a number or "unlimited"), the live usage, what is
remaining, the countingRule, what frees it (hint) and the status a refused request answers with. The
limits, usage and countingRules maps carry the same numbers keyed by name. The quotas are workflowDefinitions,
lifetimeExecutions, executionsPer24h, concurrentExecutions, workers and functionBlocks.
A write that would exceed a quota is refused with the LicenseLimitExceeded error code and the
X-Neops-License-Error: limit-exceeded header – for workflow definitions the cap counts distinct
live (package, name) tuples, so a new version of an existing workflow does not consume a slot; for workers it
counts only live registrations, pinged within WORKER_OFFLINE_THRESHOLD_MS (countingRule:
online-within-offline-threshold), so a crashed worker frees its slot once it turns offline and an unregistered one
at once. A ping that would bring an offline worker back over the cap is refused the same way.
Swagger UI
For the full list of endpoints, request/response schemas, and interactive testing,
open the Swagger UI (requires a running engine with NODE_ENV=development).

