API endpoints
Agent activities
| Method | Route | Access |
|---|---|---|
| GET | /api/agents/runs?executionId=… | Owning workflow read permission |
| GET | /api/agents/runs/{id}/events?after=0&pageSize=200 | Owning workflow read permission; max 500/page |
| GET | /api/agents/mcp-servers | Admin/Operator; no secrets |
| GET | /api/agents/mcp-servers/{id}/tools | Admin/Operator; starts a registered connection |
| PUT, DELETE | /api/agents/mcp-servers/{id} | Admin |
| GET | /api/agents/skills | Admin/Operator |
| POST | /api/agents/skills | Admin; version and base64 ZIP, max 10 MB |
| PUT | /api/agents/skills/{id}/enabled | Admin; { "enabled": true } |
| DELETE | /api/agents/skills/{id} | Admin |
Events persist before live notification and contain a per-run sequence and optional
member ID. Catch up with after after reconnect. MCP writes accept write-only secrets
and require the current updatedAt when editing. Skill versions are immutable.
General
The REST API manages workflows, executions, infrastructure and administration. In the local development environment it runs on port 5000. Live status is pushed over SignalR at /hubs/execution. Mutating workflow endpoints return 423 Locked if the calling user does not hold the edit lock; disable is exempt from that.
JSON format: property names use
camelCase. Enum values are serialized as the .NET name in PascalCase, for example"role":"Admin"and"status":"Succeeded". Sign-in uses the httpOnly cookienp_authby default.curlstores and sends it with-c cookie.jar -b cookie.jar. The examples use$NP = "http://localhost:5000".
The Bash examples below assume a login following Authentication and a cookie.jar dedicated to this server. Define this function before running them; it reads the current CSRF value, including after a refresh:
NP=http://localhost:5000
csrf_token() {
awk '$6 == "np_csrf" {sub(/\r$/, "", $7); print $7}' cookie.jar
}
Workflows
| Endpoint | Purpose |
|---|---|
GET /api/workflows | The list (an array, 500-row cap, filtered by folder RBAC). Rows carry no definitionJson — read a single workflow for the graph |
GET /api/workflows/paged | Paged list for the UI: page, pageSize (1–200, default 50), folderId, search (name and description), sortBy/sortDir (default updated/desc), up to 10 ids; filtered by folder RBAC; returns {items, page, pageSize, total, totalPages}. Deliberately has neither an np command nor an MCP tool — np workflow list covers the same workflows |
GET /api/workflows/names | Id and name only, ordered by name, filtered by folder RBAC. For surfaces that offer workflow names without rendering anything else — the executions filter uses it instead of the full list. Deliberately has neither an np command nor an MCP tool — np workflow list covers the same names |
POST /api/workflows | Create (Admin/Operator) — 201 |
PUT /api/workflows/{id} | Update — 204 (423 without the lock, 409 on a version conflict) |
DELETE /api/workflows/{id} | Delete (Admin) — 204 |
POST /{id}/execute | Start a run — 202 + ExecutionId |
POST /{id}/duplicate | Duplicate — 201 (the copy is born disabled) |
POST /{id}/enable / disable | The kill switch — 204 |
POST /{id}/cancel-all | Cancel every running and pending execution — 200 |
POST /{id}/lock / unlock | The edit lock — 200 |
POST /{id}/publish | Save + enable + unlock (atomically) — 200 |
POST /{id}/force-unlock | Admin only, breaks someone else's lock — 200 |
PUT /{id}/concurrency-limit | Admin/Operator. Body {"maxConcurrentExecutions": N} (1..1000, null = unlimited, the field is required) — 204. No edit lock, no version bump |
# The list + a single workflow
curl -s -b cookie.jar "$NP/api/workflows" | jq '.[0] | {id,name,isEnabled,version,folderPath}'
curl -s -b cookie.jar "$NP/api/workflows/by-name/deploy-prod" | jq '{id,name,isEnabled}'
# Create (definitionJson = a JSON object as a string, ≤5 MiB, depth ≤64)
curl -s -b cookie.jar -H "X-CSRF-Token: $(csrf_token)" -X POST "$NP/api/workflows" \
-H 'Content-Type: application/json' \
-d '{ "name": "Deploy App",
"description": "Deploys the web app",
"definitionJson": "{\"nodes\":[],\"edges\":[]}",
"folderId": null }'
# Update (requires the lock)
curl -s -b cookie.jar -H "X-CSRF-Token: $(csrf_token)" -X PUT "$NP/api/workflows/21f1c0d4-..." \
-H 'Content-Type: application/json' \
-d '{ "name": "Deploy App v2", "description": "updated",
"definitionJson": "{\"nodes\":[...],\"edges\":[...]}", "expectedVersion": 3 }' -i # 204
# Publish (atomic), duplicate, cancel-all
curl -s -b cookie.jar -H "X-CSRF-Token: $(csrf_token)" -X POST "$NP/api/workflows/21f1c0d4-.../publish" \
-H 'Content-Type: application/json' \
-d '{ "name": "Deploy App", "description": null,
"definitionJson": "{\"nodes\":[...],\"edges\":[...]}", "expectedVersion": 4 }'
curl -s -b cookie.jar -H "X-CSRF-Token: $(csrf_token)" -X POST "$NP/api/workflows/21f1c0d4-.../duplicate"
curl -s -b cookie.jar -H "X-CSRF-Token: $(csrf_token)" -X POST "$NP/api/workflows/21f1c0d4-.../cancel-all" # {"total":3,"signalled":2}
The 409 lock-conflict body: {"message":"Workflow is already locked by alice.","lockedByUserName":"alice","lockedAt":"..."}. A 409 version conflict: {"code":"workflow_version_conflict","currentVersion":4}. PUT and publish require expectedVersion, the version the draft is based on; when it differs from the stored one the server answers with this 409 and overwrites nothing.
Versions & contract
| Endpoint | Purpose |
|---|---|
GET /{id}/versions | The version history |
GET /{id}/versions/{v} | A specific version |
POST /{id}/rollback/{v} | Roll back — body {"reason": "..."} |
GET /{id}/contract | The input/output contract |
GET /by-name/{name}/contract | By-name lookup (exact case wins, otherwise case-insensitive; ambiguous → 409) |
curl -s -b cookie.jar "$NP/api/workflows/21f1c0d4-.../versions" | jq '.[0]'
curl -s -b cookie.jar -H "X-CSRF-Token: $(csrf_token)" -X POST "$NP/api/workflows/21f1c0d4-.../rollback/12" \
-H 'Content-Type: application/json' -d '{"reason":"revert bad config"}'
# Contract: inputs from manualTrigger.parameters + outputs from returnData + system
curl -s -b cookie.jar "$NP/api/workflows/by-name/deploy-prod/contract" | jq
# { "workflowName":"Deploy App", "hasManualTrigger":true,
# "inputs":[{"name":"version","type":"string","required":true,...}],
# "outputs":[{"name":"__executionId","source":"system"},{"name":"deployResult","source":"single"}] }
Callers without Edit permission on the workflow's folder see "***" instead of every non-null input default. The mask only affects the display: a parameter left unset still receives the real default when the workflow runs.
Step test & coverage
| Endpoint | Purpose |
|---|---|
POST /{id}/steps/{stepId}/test | A single step test |
GET .../test-context | The test context (?executionId=) |
GET .../test-context/runs | Available runs |
GET /{id}/coverage?windowDays=N | Step coverage |
GET /{id}/step-health | Step health |
GET /{id}/step-stats?windowDays=N | Step statistics |
# Test a step with mock variables + a config override
curl -s -b cookie.jar -H "X-CSRF-Token: $(csrf_token)" -X POST "$NP/api/workflows/21f1c0d4-.../steps/runHealth/test" \
-H 'Content-Type: application/json' \
-d '{ "mockVariables": { "checkDisk.output": "7", "checkDisk.param.freeGb": "7" },
"configOverride": { "script": "Get-Process", "timeoutSeconds": 30 } }' | jq
# { "success":true, "output":"COMPUTERNAME=SRV01",
# "outputParameters":{"computerName":"SRV01"}, "durationMs":412.3 }
curl -s -b cookie.jar "$NP/api/workflows/21f1c0d4-.../coverage?windowDays=7" | jq
Instead of configOverride, which replaces only data.config of the saved node, stepOverride sends the complete current node.data (activity type, config, machine, credential, output variable); omitted target or credential fields clear the saved values. The two overrides are mutually exclusive, and either requires Edit permission plus your own active edit lock. A selected machine that no longer exists is rejected.
Import/export
| Endpoint | Purpose |
|---|---|
GET /export | Bulk export |
GET /{id}/export | Single export |
POST /import?folderId={guid} | Import (name collision → the suffix " (Imported 2)"; folderId optional, absent → root, RBAC = edit on the target folder) |
POST /import-scorch?folderId={guid} | SCOrch import (.ois_export; the same folder targeting) |
curl -s -b cookie.jar "$NP/api/workflows/21f1c0d4-.../export" -o deploy.envelope.json
import_result="$(curl -s -b cookie.jar -H "X-CSRF-Token: $(csrf_token)" -X POST "$NP/api/workflows/import" \
-H 'Content-Type: application/json' --data-binary @deploy.envelope.json)"
workflow_id="$(printf '%s' "$import_result" | jq -r '.workflows[0].id')"
# Import is always disabled; only this explicit second call arms the workflow.
curl -s -o /dev/null -w '%{http_code}\n' -b cookie.jar -H "X-CSRF-Token: $(csrf_token)" -X POST \
"$NP/api/workflows/$workflow_id/enable"
Both import endpoints always create disabled workflows and return their ids in workflows[].id.
POST /api/workflows/{id}/enable is idempotent, requires Admin/Operator plus edit permission on the
workflow folder, and succeeds with 204. An edit lock or an unsafe workflow definition still blocks
activation visibly. Envelope type: nodepilot-workflow-export/v1. Secrets are redacted here (***)
— a sharing artifact, not a DR artifact.
Executions
| Endpoint | Purpose |
|---|---|
GET /api/executions | The list (?workflowId=&activeOnly=&terminalOnly=&includePayloads=, paged: {items, page, pageSize, total, totalPages}). includePayloads is the only one of these that does not filter but changes the response shape: it defaults to true, and false returns returnData and inputParametersJson as null. sortBy (started, status, workflow, trigger, startedBy, steps, duration) and sortDir (asc/desc, default started/desc) sort on the server across all pages; unknown values return 400. Purely narrowing parameters (page, pageSize, status, search) are deliberately left out of this table. |
GET /api/executions/{id} | A single execution |
GET /api/executions/{id}/steps | The steps of an execution |
POST /api/executions/{id}/cancel / retry / resume | A single run |
GET /api/executions/{id}/paused-steps | The steps a debug run is paused on, for rebuilding the debugger after a page reload (workflow read permission) |
curl -s -b cookie.jar "$NP/api/executions?workflowId=21f1c0d4-...&activeOnly=true" | jq '.items[0]'
curl -s -b cookie.jar "$NP/api/executions/7e3f..." | jq '{status,startedAt,completedAt,triggeredBy}'
curl -s -b cookie.jar "$NP/api/executions/7e3f.../steps" | jq '.[] | {stepId,status,durationMs}'
curl -s -b cookie.jar -H "X-CSRF-Token: $(csrf_token)" -X POST "$NP/api/executions/7e3f.../cancel" -i # 204
curl -s -b cookie.jar -H "X-CSRF-Token: $(csrf_token)" -X POST "$NP/api/executions/7e3f.../retry" -i # 202 + Location
The resume body (a debug pause): {"stepId":"runHealth","mode":"continue|stepOver|stop","overrides":{"vars.targetHost":"srv02"}} → 204.
Machines & credentials
| Endpoint | Purpose |
|---|---|
GET/POST/PUT/DELETE /api/machines | Machines (POST/PUT Admin/Operator, DELETE Admin) |
GET /api/machines/options | Authenticated machine selectors: nine configuration/connectivity fields, sorted by name; no workflow or activity statistics |
POST /{id}/test | A machine connection test — body {"credentialId": null} |
GET/POST/PUT/DELETE /api/credentials | Credentials (DELETE Admin) |
# Create a machine
curl -s -b cookie.jar -H "X-CSRF-Token: $(csrf_token)" -X POST "$NP/api/machines" -H 'Content-Type: application/json' \
-d '{ "name":"SRV-PROD-01", "hostname":"srv-prod-01.contoso.com",
"winRmPort":5985, "useSsl":false,
"defaultCredentialId":"4c2a-...", "tags":"prod;web" }'
# Connection test (credentialId null = the machine default)
curl -s -b cookie.jar -H "X-CSRF-Token: $(csrf_token)" -X POST "$NP/api/machines/9f1a.../test" \
-H 'Content-Type: application/json' -d '{"credentialId":null}'
# { "success":true, "computerName":"SRV-PROD-01", "credentialUsed":"svc-winrm" }
# Credential (password minimum 8 characters, never returned)
curl -s -b cookie.jar -H "X-CSRF-Token: $(csrf_token)" -X POST "$NP/api/credentials" -H 'Content-Type: application/json' \
-d '{ "name":"svc-winrm", "username":"CONTOSO\\svc-winrm", "password":"p@ssw0rd!", "domain":null }'
# 201 → { "id":"...", "name":"svc-winrm", "username":"CONTOSO\\svc-winrm", "domain":null }
Global variables
GET /api/global-variables (Admin/Operator), POST/PUT/DELETE (Admin). Name pattern [A-Za-z0-9_-]{1,100}. Secrets are stored but never returned ("value":"***").
Folders: GET /api/global-variable-folders (Admin/Operator), POST, PUT/DELETE /{id}, POST /{id}/move (Admin). POST /api/global-variables/{id}/move-folder moves a variable (Admin).
curl -s -b cookie.jar -H "X-CSRF-Token: $(csrf_token)" -X POST "$NP/api/global-variables" -H 'Content-Type: application/json' \
-d '{ "name":"API_ENDPOINT", "value":"https://api.example.com",
"isSecret":false, "description":"Upstream API base URL" }'
# Create a secret — the response masks the value
curl -s -b cookie.jar -H "X-CSRF-Token: $(csrf_token)" -X POST "$NP/api/global-variables" -H 'Content-Type: application/json' \
-d '{ "name":"SIGNING_KEY", "value":"-----BEGIN PRIVATE KEY-----...",
"isSecret":true, "description":null }'
# 201 → { ..., "value":"***", "isSecret":true }
Authentication
| Endpoint | Purpose |
|---|---|
POST /api/auth/login / logout / refresh | Password/LDAP login and the session lifecycle |
POST /api/auth/windows | Windows Negotiate/Kerberos |
GET /api/auth/oidc / oidc/callback | Release-gated OIDC authorization code + PKCE |
GET /api/auth/me / methods | Profile / authentication-method discovery |
/api/scim/v2/Users / Groups | Release-gated SCIM 2.0 provisioning |
/api/scim/v2/ServiceProviderConfig / ResourceTypes / Schemas | SCIM 2.0 discovery |
# Login — the token lands in the httpOnly np_auth cookie (-c stores it)
curl -s -c cookie.jar -X POST "$NP/api/auth/login" \
-H 'Content-Type: application/json' \
-d '{ "username":"admin", "password":"s3cret-pass" }'
# Without the header → { "userId":"...", "username":"admin", "role":"Admin" } (the token is in the cookie only)
# The current identity and the available authentication paths
curl -s -b cookie.jar "$NP/api/auth/me" # {"id":"...","username":"admin","role":"Admin"}
curl -s "$NP/api/auth/methods" # {"local":true,"ldap":false,"windows":false,"windowsEndpoint":null,"oidc":false,"oidcEndpoint":null,"oidcDisplayName":null}
Details on server-side sessions, AD SSO Preview, OIDC and refresh: Authentication & roles.
Audit
GET /api/audit (Admin, cursor pagination), GET /api/audit/export?format=csv|ndjson.
# Filters + cursor pagination (take is at most 500)
curl -s -b cookie.jar "$NP/api/audit?action=WORKFLOW_PUBLISHED&since=2026-06-01T00:00:00Z&take=50" | jq
# { "items":[...], "nextCursor":{"timestamp":"...","id":"..."} } → pass nextCursor as afterTs/afterId
# The next page
curl -s -b cookie.jar "$NP/api/audit?afterTs=2026-06-25T10:00:00Z&afterId=7e3f...&take=50"
# Export (CSV by default; the NDJSON stream also uses camelCase keys)
curl -s -b cookie.jar "$NP/api/audit/export?format=ndjson&since=2026-06-01T00:00:00Z" -o audit.ndjson
Audit codes follow VERB_NOUN. The complete list: Audit log.
Triggers & webhooks
| Endpoint | Purpose |
|---|---|
POST /api/trigger/{workflowNameOrId} | The external trigger (X-Api-Key; the key scope has to cover the workflow GUID, and an active manualTrigger is required) |
POST|GET|PUT|DELETE /api/webhooks/{workflow}/{path} | The webhook (the verb has to match webhookTrigger.method) |
# External trigger — anonymous, but limited to workflow GUIDs by the integration key. Optional Idempotency-Key (24 h TTL, isolated per key principal)
curl -s -X POST "$NP/api/trigger/nightly-reconcile" \
-H 'X-Api-Key: xyz' \
-H 'Idempotency-Key: ci-1719100000' \
-H 'Content-Type: application/json' \
-d '{ "parameters":{"day":"2026-06-25"}, "timeoutSeconds":null, "debug":false }'
# 202 → ExecutionResponse + Location; a replay → 200 + Idempotent-Replayed: true
# Webhook — X-Webhook-Secret if the node has a secret configured
curl -s -X POST "$NP/api/webhooks/deploy-prod/github-push" \
-H 'X-Webhook-Secret: whsec-...' \
-H 'Content-Type: application/json' \
-d '{"ref":"refs/heads/main","after":"abcd123"}'
# 202 → {"workflowId":"...","executionId":"...","message":"Triggered"}
Every webhook rejection path (missing/wrong secret, disabled, path/method mismatch, maintenance window) returns a uniform 404 {"message":"Webhook endpoint not found"} — no leak of which condition failed.
Observability & diagnostics
| Endpoint | Purpose |
|---|---|
GET /api/observability/config|query|query_range|summary | Prometheus/OTel queries |
GET /api/observability/dashboards/{key} | A dashboard from the fixed metrics catalog (all roles) |
GET /api/diagnostics/support-log|support-log/download|support-events|support-events/export|app-log|app-log/download | Diagnostics (Admin) |
# config is anonymous; query/query_range/summary are Admin/Operator
curl -s "$NP/api/observability/config" | jq
curl -s -b cookie.jar "$NP/api/observability/query?query=up%7Bjob%3D%22nodepilot%22%7D" | jq
curl -s -b cookie.jar "$NP/api/observability/query_range?query=rate(nodepilot_workflows_total%5B5m%5D)&start=1719100000&end=1719103600&step=1m"
curl -s -b cookie.jar "$NP/api/observability/summary" | jq '.panels[] | {key,value}'
query is limited to 8 KiB, with a metric-name prefix allow-list, and a __name__ selector is rejected. 503 {"message":"Prometheus query endpoint is not configured."} if it is not configured.
Backup, users, maintenance, folders, settings, database admin
# Backup — multipart for preview/restore, the passphrase as a form field
curl -s -b cookie.jar "$NP/api/backup/manifest" | jq # {"sections":[{"section":"credentials","count":12},...]}
curl -s -b cookie.jar -H "X-CSRF-Token: $(csrf_token)" -X POST "$NP/api/backup/export" -H 'Content-Type: application/json' \
-d '{ "sections":["credentials","globalVariables","workflows","settings"],
"passphrase":"correct-horse-battery-staple" }' -o backup.npbackup
# Restore: file + passphrase + policy (skip|rename|overwrite, overridable per section)
curl -s -b cookie.jar -H "X-CSRF-Token: $(csrf_token)" -X POST "$NP/api/backup/restore" \
-F "file=@backup.npbackup" -F "passphrase=correct-horse-battery-staple" -F "policy=skip,Users=Overwrite"
# Users
curl -s -b cookie.jar -H "X-CSRF-Token: $(csrf_token)" -X POST "$NP/api/users" -H 'Content-Type: application/json' \
-d '{ "username":"alice", "password":"p@ssw0rd!", "role":"Operator" }' # 201
# Settings — ETag-gated (If-Match required)
ETAG=$(curl -s -b cookie.jar -D - "$NP/api/admin/settings/Smtp" | tr -d '\r' | awk -F': ' '/^ETag:/ {print $2}')
curl -s -b cookie.jar -H "X-CSRF-Token: $(csrf_token)" -X PUT "$NP/api/admin/settings/Smtp" \
-H 'Content-Type: application/json' -H "If-Match: $ETAG" -d @smtp.json
# 428 without If-Match; 412 on a mismatch; 400 from boot validation "would prevent booting"
# In a cluster: PUT /api/admin/settings/Authentication → 409 CLUSTER_CONFIG_AS_CODE_REQUIRED
| Area | Endpoints |
|---|---|
| Backup | GET /api/backup/manifest, POST /export, POST /{preview|restore} (Admin, multipart) |
| Users | GET/POST /api/users, PUT/DELETE /api/users/{id}, POST /{id}/reactivate (Admin) |
| SCIM groups | GET /api/admin/scim-groups/tombstones, POST /{id}/reactivate (Admin) |
| Custom activities | GET /api/custom-activities (all roles see enabled entries; Admin/Operator also drafts with ?includeDisabled=true), GET /{id}, GET /{id}/versions, GET /export, POST, PUT/DELETE /{id}, POST /{id}/rollback/{version}, POST /import (Admin/Operator; once enabled, only an Admin may change an entry), POST /{id}/enable|disable (Admin) |
| Alerting rules | GET /api/alerting/catalog|rules|rules/{id}|deliveries, POST /preview-filter|preview-rule (Admin/Operator), POST /rules, PUT/DELETE /rules/{id}, POST /rules/{id}/enable|disable|test-fire (Admin) — see Alerting |
| System alert policies | GET /api/alerting/system/catalog|policies|policies/{id}, POST /preview (Admin/Operator), POST /policies, PUT/DELETE /policies/{id}, POST /policies/{id}/enable|disable|test-fire (Admin) |
| Dependencies | GET /api/dependencies/objects (q, kind, state=problems|unused, sort=name|usage, cursor, limit), GET /{kind}/{id}, GET /{kind}/{id}/evidence — kind is workflow|machine|credential|variable|customActivity; all roles, scoped to the readable folders |
| Operations | GET /api/operations/graph?windowMinutes=30|60 — the live operations view; all roles, scoped to the readable folders |
| Maintenance windows | GET /api/maintenance-windows, GET /{id}, GET /affecting/{workflowId}, POST, PUT/DELETE /{id} |
| Shared folders | GET/POST /api/shared-workflow-folders, PUT/DELETE /{id}, POST /{id}/move, POST /api/workflows/{workflowId}/move-folder |
| Folder permissions | GET/POST /api/shared-workflow-folders/{folderId}/permissions, PUT/DELETE /{permissionId} |
| Settings | GET /api/admin/settings, GET|PUT /{section}, GET /status|system-info|effective-sizing, POST /test/smtp|test/llm|test/ldap, POST /restart-service (Admin; restart-service answers 202, or 409 SERVICE_RESTART_UNAVAILABLE when the server cannot restart itself; an Authentication PUT in a cluster returns 409) |
| Database admin | GET /api/dbadmin/tables, GET|PATCH|DELETE /tables/{name}/rows, GET /info, POST /query (Admin) |
| Dashboard | GET /api/stats/dashboard, GET /api/stats/failure-causes?windowHours=N (1..720, default 24 — recent failures grouped by their normalized message; folder-scoped like the dashboard), GET /api/stats/sidebar-counts (three nav-badge counters for the SPA, folder-scoped; deliberately has neither an np command nor an MCP tool — np stats dashboard covers the same numbers), GET /api/stats/duration-trend?windowHours=N&workflowId= (1/24/168/720, default 24, anything else falls back to 24 — median and P95 execution duration per bucket for all workflows or one, folder-scoped; from 24 h upward taken from precomputed hourly histograms, accurate to within a few percent; SPA chart series, deliberately without an np command or MCP tool — np stats dashboard reports the same executions) |
| Activity catalog | GET /api/activity-catalog |
| Scheduler | GET /api/triggers/schedule/next-fires |
| System | GET /api/system/host-info (all roles) |
| AI | POST /api/ai/generate-script|generate-workflow|generate-agent-team (Admin/Operator), POST /api/ai/chat (all roles; applying changes is Admin/Operator), POST /api/ai/chat/applied + GET /api/ai/chat/activity/{workflowId} (Admin/Operator, folder RBAC), GET /api/ai/knowledge/capabilities + POST /api/ai/knowledge/ask (the global AI chat, all roles; the sources depend on the role) — opt-in, SSE streaming |
| Secrets | POST /api/secrets/reencrypt (Admin, no body) |
The shared-folder permission grant body: {"principalType":"User","principalKey":"<guid>","role":"FolderEditor"} — roles FolderViewer|FolderOperator|FolderEditor|FolderAdmin, principalType User|Group (Group = an AD SID S-1-5-21-...).
The maintenance-window create body:
{ "name":"Saturday Patch Reboot", "isEnabled":true, "mode":"Blackout",
"scopeKind":"Global", "recurrence":"OneTime",
"oneTimeStartUtc":"2026-06-27T22:00:00Z", "oneTimeEndUtc":"2026-06-28T06:00:00Z",
"weeklyDaysMask":0, "timeZoneId":"UTC", "targets":null }
Mode: Blackout|AllowOnly. ScopeKind: Global|Folders|Workflows (with Folders/Workflows, Targets has to be non-empty). Recurrence: OneTime|Weekly|Cron — with Cron, cronExpression (Quartz syntax with a seconds field, e.g. 0 0 3 ? * SAT) and durationMinutes (1..10080) are mandatory; the window is active for durationMinutes on every fire, interpreted in timeZoneId.
AI (opt-in)
Llm:Enabled=false by default. 503 {"code":"LLM_DISABLED",...} when disabled, and 503 {"code":"LLM_NO_ACTIVE_PROFILE",...} when enabled but no LLM profile is selected.
Script generation returns Server-Sent Events: concatenate delta.text fragments to obtain the script; done contains model and durationMs. Failures before the stream starts use normal HTTP errors; later failures arrive as an error event with code and message. curl -N displays fragments without output buffering.
# Generate a script (with upstream variable context)
# The optional editor context additionally requires includeCurrentScript:true; without that flag
# the server ignores a supplied currentScript, because it can contain passwords or tokens.
curl -sN -b cookie.jar -H "X-CSRF-Token: $(csrf_token)" -X POST "$NP/api/ai/generate-script" -H 'Content-Type: application/json' \
-d '{ "prompt":"Write a PowerShell step that checks free disk space",
"workflowId":"21f1c0d4-...", "stepId":"runScript_1",
"upstreamVariables":[
{"stepId":"collectInfo","label":"Collect Info → $hostname",
"variable":"collectInfo.param.hostname","expression":"{{collectInfo.param.hostname}}","type":"string"}] }'
# 200 Content-Type: text/event-stream
# event: delta
# data: {"text":"Get-PSDrive ..."}
#
# event: done
# data: {"model":"gpt-4o","durationMs":1820}
# Generate a workflow
curl -s -b cookie.jar -H "X-CSRF-Token: $(csrf_token)" -X POST "$NP/api/ai/generate-workflow" -H 'Content-Type: application/json' \
-d '{ "prompt":"A workflow that checks disk space and emails on low" }'
# 200 → {"definitionJson":"{...}","suggestedName":"Check Disk Space","nodeCount":4,...}
Health
| Endpoint | Purpose |
|---|---|
GET /healthz/live | Liveness |
GET /healthz/ready | Database readiness; an immediate 503 on armed or unavailable, with no directory dependency |
GET /healthz/database | A database status report for the UI: always 200 with {status: ok|armed|unavailable, sinceUtc, reason} |
GET /healthz/directory | A separate LDAPS/service-bind status |
GET /healthz/leader | The HA leader probe (fail-closed) |
With the database breaker open, /api, new hub transports, the OIDC callback and protected metrics
are terminated before any database or authentication access. These HTTP paths use 503, Retry-After
and the same body:
{
"code": "DATABASE_UNAVAILABLE",
"message": "...",
"retryAfterSeconds": 15,
"reason": "Unreachable",
"retryable": true
}
reason is Unknown, Unreachable, Wedged or RejectedByServer; the last of those sets
retryable: false. A single command timeout uses the same contract with code: DATABASE_TIMEOUT, a
five-second retry interval and no reason. On an already established hub, methods return the same
codes as SignalR errors.
curl -s "$NP/healthz/live" ; echo
curl -s "$NP/healthz/ready" ; echo
curl -s "$NP/healthz/database"; echo
curl -s "$NP/healthz/directory"; echo
curl -s "$NP/healthz/leader"; echo # the HA leader probe, anonymous
All healthz endpoints are AllowAnonymous.