Your First Function Block
In this guide you will build a function block from scratch — a small, self-contained
unit of automation logic that the neops platform can schedule and execute. By the end
you will understand parameters, results, registration, and the run() method.
Python concepts you'll see in this guide
This guide uses a few Python features that may be new to you. Here is a quick orientation:
Type hints — annotations like text: str or count: int that tell Python (and your editor) what type each variable should be. They enable autocomplete, catch bugs early, and are required by the framework.
Pydantic — a popular Python library for data validation. You define a class with typed fields, and Pydantic ensures the data always matches. If someone passes a number where a string is expected, Pydantic raises a clear error automatically.
async def — the framework uses Python's async features internally. For now, treat async def as boilerplate: your code inside run() works just like regular Python. You can use normal for loops, if statements, and library calls.
Generics (FunctionBlock[X, Y]) — the square brackets tell the framework which input and output types this block uses. Think of it as a template: FunctionBlock[InputType, OutputType]. Your editor uses this for autocomplete.
What you'll build
An Echo function block that receives a piece of text and returns it unchanged. It is deliberately simple so you can focus on the structure rather than the logic. Every function block you write later — configuring interfaces, collecting facts, pushing templates — follows the exact same pattern.
Step 1: Imports
Every function block needs a few imports from the SDK:
from neops_worker_sdk.function_block.function_block import (
FunctionBlock,
FunctionBlockAcquireResult,
FunctionBlockParams,
FunctionBlockResult,
FunctionBlockResultData,
)
from neops_worker_sdk.registry.decorator import register_function_block
from neops_worker_sdk.registry.registration import Registration
from neops_worker_sdk.workflow.workflow_context import WorkflowContext
These give you the base classes for parameters, results, the function block itself, and the registration decorator.
Step 2: Define parameters and results
Function blocks communicate through typed data classes. Parameters are the inputs your block receives when the workflow engine schedules it; results are the structured outputs it returns.
Parameters
FunctionBlockParams is a Pydantic model. By writing text: str, you are saying:
"this block requires a string input called text." Pydantic validates this
automatically — if someone passes a number instead of a string, you get a clear error.
Results
FunctionBlockResultData works the same way. Whatever you define here is persisted
on the blackboard and available to downstream steps in the workflow.
Step 3: Register the function block
Registration tells the platform what your block is called, which entities it operates on, and how it behaves. You do this with a decorator above the class:
@register_function_block(
Registration(
name="echo",
description="Return the provided text without modification.",
package="fb.examples.neops.io",
version=(1, 0, 0),
run_on="global",
fb_type="execute",
param_cls=EchoParameters,
result_cls=EchoResult,
is_pure=True,
is_idempotent=True,
)
)
Key fields:
| Field | Purpose |
|---|---|
name |
Unique identifier for this function block |
package |
Reverse-DNS namespace to avoid collisions across teams |
version |
Semantic version tuple (major, minor, patch) |
run_on |
Entity scope — "device", "interface", "group", or "global" |
fb_type |
Operation category — "execute", "facts", "check", "configure", "none" |
param_cls / result_cls |
Links to the parameter and result models defined above |
is_pure |
True if the block has no side effects (e.g. reading config, not writing it) |
is_idempotent |
True if running it twice produces the same outcome |
Note
is_pure and is_idempotent help the workflow engine decide when it is safe to
retry or cache. You will learn more about these in the
Pure and Idempotent guide.
Step 4: Implement the logic
With the metadata in place, write the class that does the actual work:
class Echo(FunctionBlock[EchoParameters, EchoResult]):
async def run(self, params: EchoParameters, context: WorkflowContext) -> FunctionBlockResult[EchoResult]:
del context
return FunctionBlockResult(
message="Echo executed successfully.",
success=True,
data=EchoResult(echoed_message=params.message),
)
async def acquire(self, params: EchoParameters) -> FunctionBlockAcquireResult:
del params
return FunctionBlockAcquireResult(message="No resources required.", success=True, acquires=None)
Breaking this down:
FunctionBlock[EchoParameters, EchoResult]— the square brackets tell the framework (and your editor) which parameter and result types this block uses. This enables full autocomplete onparams.messageanddata.echoed_message.async def run(...)— the entry point the worker calls. Theasynckeyword is required by the framework; your code inside works like regular Python. It receives validatedparamsand aWorkflowContextthat carries entity data.FunctionBlockResult— wrap your response with asuccessflag, a human-readablemessage, and your typeddata.del context/del params— a Python idiom that tells linters and readers "this argument is intentionally unused." The statement discards the local reference but has no effect on the actual data. An alternative is to prefix the argument with an underscore (_context).acquire()— called beforerun()to request additional data from the CMS or lock shared resources. For blocks that don't need this (like our Echo), return a simple success. The method is required by the framework — every function block must implement it.
No custom __init__
The FunctionBlock.__init__ method is marked @final — you cannot override it.
This ensures consistent logger setup and lifecycle management. If your block needs
initialization state, use the acquire() phase or set it up in run().
Keep run() focused
A function block should do one thing well. If you find yourself writing
hundreds of lines in run(), consider splitting the work into multiple
blocks that compose inside a workflow.
What happens next
When you deploy a worker, it automatically discovers every decorated function block, registers them with the platform, and polls for jobs. But you do not need a full neops instance to verify your code works — the next section shows you how to test it locally with pytest.
Full example
Here is the complete Echo function block in one file:
from neops_worker_sdk.function_block.function_block import (
FunctionBlock,
FunctionBlockAcquireResult,
FunctionBlockParams,
FunctionBlockResult,
FunctionBlockResultData,
)
from neops_worker_sdk.registry.decorator import register_function_block
from neops_worker_sdk.registry.registration import Registration
from neops_worker_sdk.workflow.workflow_context import WorkflowContext
class EchoParameters(FunctionBlockParams):
message: str
class EchoResult(FunctionBlockResultData):
echoed_message: str
@register_function_block(
Registration(
name="echo",
description="Return the provided text without modification.",
package="fb.examples.neops.io",
version=(1, 0, 0),
run_on="global",
fb_type="execute",
param_cls=EchoParameters,
result_cls=EchoResult,
is_pure=True,
is_idempotent=True,
)
)
class Echo(FunctionBlock[EchoParameters, EchoResult]):
async def run(self, params: EchoParameters, context: WorkflowContext) -> FunctionBlockResult[EchoResult]:
del context
return FunctionBlockResult(
message="Echo executed successfully.",
success=True,
data=EchoResult(echoed_message=params.message),
)
async def acquire(self, params: EchoParameters) -> FunctionBlockAcquireResult:
del params
return FunctionBlockAcquireResult(message="No resources required.", success=True, acquires=None)
See it in action
The echo function block you just built is used in the Workflow Engine's Your First Workflow tutorial. Deploy the hello-world workflow, start your worker, and watch the engine orchestrate the function block you just wrote.
Next: Connecting to Devices to open your first device connection, or jump to Test your function block to verify it works locally with pytest.