Your First Workflow
This guide walks you through deploying and executing a minimal workflow. You will:
- Write a workflow YAML file
- Deploy it to the engine
- Execute it
- Inspect the results
The Workflow
Here is the simplest possible workflow -- it runs a single echo function block with global scope (no CMS connection needed):
# A minimal workflow that runs a single function block.
# Uses seedEntity: global so it works without CMS -- ideal for getting started.
label: hello_world
name: hello_world
package: wf.example.neops.io
majorVersion: 1
minorVersion: 0
patchVersion: 0
seedEntity: global
type: workflow
steps:
- type: functionBlock
label: echo
functionBlock: "fb.examples.neops.io/echo:1.0.0"
parameters:
message: "Hello from the neops workflow engine!"
Let's break this down:
| Field | Purpose |
|---|---|
label |
Unique identifier within the execution (used by other steps to reference results) |
name |
Human-readable name |
package |
Namespace (reverse domain notation, like Java packages) |
majorVersion / minorVersion / patchVersion |
Semantic version |
seedEntity |
What type of entity this workflow operates on (device, interface, group, or global) |
type |
Always workflow for the root |
steps |
The sequence of operations to execute |
The single step references the echo function block at version 1.0.0 and passes a static message. Since this is a global workflow, no device context is needed -- it runs exactly once.
Dynamic parameters with JMESPath
In device-scoped workflows, you can use JMESPath expressions like
{{ context.device.hostname }} to interpolate runtime data into parameters.
JMESPath is a query language for JSON -- try the
interactive tutorial (about 10 minutes).
You'll see this in action in the Show Version example.
Build the echo function block yourself
The echo function block used here is implemented in Python using the Worker SDK.
See Your First Function Block
to understand how it works and write your own.
Deploy the Workflow
Use the API to register this workflow definition with the engine:
curl -X POST http://localhost:3030/workflow-definition \
-H "Content-Type: application/json" \
-d @- << 'EOF'
{
"workflow": {
"label": "hello_world",
"name": "hello_world",
"package": "wf.example.neops.io",
"majorVersion": 1,
"minorVersion": 0,
"patchVersion": 0,
"seedEntity": "global",
"type": "workflow",
"steps": [
{
"type": "functionBlock",
"label": "echo",
"functionBlock": "fb.examples.neops.io/echo:1.0.0",
"parameters": {
"message": "Hello from the neops workflow engine!"
}
}
]
}
}
EOF
Converting YAML to JSON
The REST API and Monitor App currently accept JSON only. We recommend authoring workflows in YAML for readability and converting when needed:
With yq (recommended):
# Convert and pipe directly to the API
yq -o=json hello-world.workflow.yaml | \
jq '{workflow: .}' | \
curl -X POST http://localhost:3030/workflow-definition \
-H "Content-Type: application/json" -d @-
# Or just convert to a file
yq -o=json hello-world.workflow.yaml > hello-world.workflow.json
Online: Paste your YAML at json2yaml.com (works both ways) or yaml-online-parser.appspot.com.
Install yq via brew install yq, pip install yq, or see
github.com/mikefarah/yq.
Using the Monitor App
You can also deploy workflows through the Monitor App at http://localhost:5173. Navigate to Workflow Definitions and use the create form.
The response returns the workflow definition with its assigned ID:
{
"id": 1,
"label": "hello_world",
"name": "hello_world",
"package": "wf.example.neops.io",
"majorVersion": 1,
"minorVersion": 0,
"patchVersion": 0,
"seedEntity": "global",
"type": "workflow",
"steps": [...]
}
Execute the Workflow
Trigger an execution by providing the workflow identifier:
curl -X POST http://localhost:3030/workflow-execution \
-H "Content-Type: application/json" \
-d '{
"workflow": "wf.example.neops.io/hello_world:1.0.0",
"executeOnParameters": {},
"parameters": {}
}'
The workflow field is the full SemVer identifier (package/name:major.minor.patch). Since this workflow uses seedEntity: global, no entity IDs are needed. For device-scoped workflows, you'd provide "executeOnParameters": { "ids": [1, 2, 3] } with CMS device IDs.
The response returns the execution with its initial state:
{
"id": 1,
"status": "NEW",
"workflow": "wf.example.neops.io/hello_world:1.0.0",
"createdAt": "2025-01-15T10:30:00.000Z"
}
Workers required
For the execution to proceed, at least one worker must be running that has the
fb.examples.neops.io/echo:1.0.0 function block registered.
Quick worker setup (requires the Worker SDK):
cd /path/to/neops-worker-sdk-py
URL_BLACKBOARD=http://localhost:3030 DIR_FUNCTION_BLOCKS=examples/getting-started/echo uv run neops_worker
The worker discovers the echo function block, registers it with the engine, and starts polling for jobs. You should see log output confirming registration.
Inspect the Execution
Check the execution state:
The response shows the current status of the execution: which state it is in and which jobs have been created.
You can also track execution in real time through the Monitor App's Executions view.
What Happened?
sequenceDiagram
participant You
participant Engine as Workflow Engine
participant BB as Blackboard
participant Worker
You->>Engine: Execute workflow
Engine->>Engine: Resolve function blocks
Engine->>BB: Create EXECUTE job (echo)
Worker->>BB: Poll for jobs
BB->>Worker: Return echo job
Worker->>Worker: Execute echo FB
Worker->>BB: Push result
BB->>Engine: Job result event
Engine->>Engine: All steps complete → COMPLETED
The engine:
- Resolved the
echofunction block reference to a registered version - Created a job on the blackboard (one job since this is a global workflow)
- A worker polled the blackboard, picked up the job, and executed the echo function block
- Results flowed back, the engine advanced the workflow, and marked it
COMPLETED
Device-scoped workflows
For workflows with seedEntity: device, the engine creates one job per device per step.
If you execute on 100 devices with 3 steps, that's up to 300 jobs distributed across workers.
Complete the loop
You've deployed and executed a workflow. The echo function block was provided by a worker
running the Worker SDK. To write your own function blocks, head to the
Worker SDK Getting Started and build one from
scratch. Then come back to compose it into a workflow.
Next: Dive into Concepts to understand workflows, transactions, the blackboard, and the execution model in depth.