Skip to content

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 most facetLimit values (default 20), with the values the filter chose always listed. The facetQ… keys of each list search inside one facet.
  • histogram: rows per time bucket and status. interval (5m, 15m, 1h, 3h, 6h, 1d, 1w from 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 as Europe/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

curl http://localhost:3030/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

curl http://localhost:3030/license/

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).