API endpoints

Agent activities

MethodRouteAccess
GET/api/agents/runs?executionId=…Owning workflow read permission
GET/api/agents/runs/{id}/events?after=0&pageSize=200Owning workflow read permission; max 500/page
GET/api/agents/mcp-serversAdmin/Operator; no secrets
GET/api/agents/mcp-servers/{id}/toolsAdmin/Operator; starts a registered connection
PUT, DELETE/api/agents/mcp-servers/{id}Admin
GET/api/agents/skillsAdmin/Operator
POST/api/agents/skillsAdmin; version and base64 ZIP, max 10 MB
PUT/api/agents/skills/{id}/enabledAdmin; { "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 cookie np_auth by default. curl stores 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

EndpointPurpose
GET /api/workflowsThe list (an array, 500-row cap, filtered by folder RBAC). Rows carry no definitionJson — read a single workflow for the graph
GET /api/workflows/pagedPaged 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/namesId 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/workflowsCreate (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}/executeStart a run — 202 + ExecutionId
POST /{id}/duplicateDuplicate — 201 (the copy is born disabled)
POST /{id}/enable / disableThe kill switch — 204
POST /{id}/cancel-allCancel every running and pending execution — 200
POST /{id}/lock / unlockThe edit lock — 200
POST /{id}/publishSave + enable + unlock (atomically) — 200
POST /{id}/force-unlockAdmin only, breaks someone else's lock — 200
PUT /{id}/concurrency-limitAdmin/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

EndpointPurpose
GET /{id}/versionsThe version history
GET /{id}/versions/{v}A specific version
POST /{id}/rollback/{v}Roll back — body {"reason": "..."}
GET /{id}/contractThe input/output contract
GET /by-name/{name}/contractBy-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

EndpointPurpose
POST /{id}/steps/{stepId}/testA single step test
GET .../test-contextThe test context (?executionId=)
GET .../test-context/runsAvailable runs
GET /{id}/coverage?windowDays=NStep coverage
GET /{id}/step-healthStep health
GET /{id}/step-stats?windowDays=NStep 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

EndpointPurpose
GET /exportBulk export
GET /{id}/exportSingle 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

EndpointPurpose
GET /api/executionsThe 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}/stepsThe steps of an execution
POST /api/executions/{id}/cancel / retry / resumeA single run
GET /api/executions/{id}/paused-stepsThe 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

EndpointPurpose
GET/POST/PUT/DELETE /api/machinesMachines (POST/PUT Admin/Operator, DELETE Admin)
GET /api/machines/optionsAuthenticated machine selectors: nine configuration/connectivity fields, sorted by name; no workflow or activity statistics
POST /{id}/testA machine connection test — body {"credentialId": null}
GET/POST/PUT/DELETE /api/credentialsCredentials (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

EndpointPurpose
POST /api/auth/login / logout / refreshPassword/LDAP login and the session lifecycle
POST /api/auth/windowsWindows Negotiate/Kerberos
GET /api/auth/oidc / oidc/callbackRelease-gated OIDC authorization code + PKCE
GET /api/auth/me / methodsProfile / authentication-method discovery
/api/scim/v2/Users / GroupsRelease-gated SCIM 2.0 provisioning
/api/scim/v2/ServiceProviderConfig / ResourceTypes / SchemasSCIM 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

EndpointPurpose
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

EndpointPurpose
GET /api/observability/config|query|query_range|summaryPrometheus/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/downloadDiagnostics (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
AreaEndpoints
BackupGET /api/backup/manifest, POST /export, POST /{preview|restore} (Admin, multipart)
UsersGET/POST /api/users, PUT/DELETE /api/users/{id}, POST /{id}/reactivate (Admin)
SCIM groupsGET /api/admin/scim-groups/tombstones, POST /{id}/reactivate (Admin)
Custom activitiesGET /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 rulesGET /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 policiesGET /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)
DependenciesGET /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
OperationsGET /api/operations/graph?windowMinutes=30|60 — the live operations view; all roles, scoped to the readable folders
Maintenance windowsGET /api/maintenance-windows, GET /{id}, GET /affecting/{workflowId}, POST, PUT/DELETE /{id}
Shared foldersGET/POST /api/shared-workflow-folders, PUT/DELETE /{id}, POST /{id}/move, POST /api/workflows/{workflowId}/move-folder
Folder permissionsGET/POST /api/shared-workflow-folders/{folderId}/permissions, PUT/DELETE /{permissionId}
SettingsGET /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 adminGET /api/dbadmin/tables, GET|PATCH|DELETE /tables/{name}/rows, GET /info, POST /query (Admin)
DashboardGET /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 catalogGET /api/activity-catalog
SchedulerGET /api/triggers/schedule/next-fires
SystemGET /api/system/host-info (all roles)
AIPOST /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
SecretsPOST /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

EndpointPurpose
GET /healthz/liveLiveness
GET /healthz/readyDatabase readiness; an immediate 503 on armed or unavailable, with no directory dependency
GET /healthz/databaseA database status report for the UI: always 200 with {status: ok|armed|unavailable, sinceUtc, reason}
GET /healthz/directoryA separate LDAPS/service-bind status
GET /healthz/leaderThe 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.

All chapters