Skip to content

Setup

Get your development environment ready to build neops function blocks.

Prerequisites

You need four things installed:

Tool Version Purpose
Python 3.12+ Runtime for function blocks
uv latest Fast Python package manager
Git any recent Clone your starting point (boilerplate or SDK repo)
Code editor any VS Code or PyCharm recommended

Installing uv

curl -LsSf https://astral.sh/uv/install.sh | sh

See docs.astral.sh/uv for other installation methods.

Choose Your Path

There are two distinct ways to set up, depending on what you are here to do:

You want to… Your path
Build your own function blocks and run them in a worker Create your own worker — most readers start here
Fix or extend the SDK itself and send a pull request Working on the SDK itself

The recommended way to start is the neops-worker-boilerplate repository — a complete, ready-to-run worker project built on the published neops_worker_sdk package. It ships example function blocks, a test suite, a workflow definition, a Dockerfile, CI, and editor debug configurations, so you start from a project that already lints, type-checks, and tests green.

Clone it under the name of your project:

git clone https://github.com/zebbra/neops-worker-boilerplate.git acme-worker
cd acme-worker
uv sync

uv sync installs everything, development tools included — no extra flags needed.

Make It Yours

The Python package my_worker/, the project name my-worker, and the function block package fb.my_worker.example.io are deliberate placeholders. Rename them all in one step:

make init-repo NAME=acme_worker

The script requires a clean git tree, renames my_worker/ to acme_worker/, rewrites every my_worker / my-worker occurrence across tracked files, and regenerates uv.lock. Everything is left uncommitted for you to review with git diff, then verify and commit:

uv sync
make lint typeCheck test

It refuses to run twice — once the placeholder is gone, the repo counts as initialized.

Underscores, not hyphens

NAME must be a lowercase Python identifier (acme_worker, not acme-worker). Function blocks are addressed as <package>/<name>:<version>, and the workflow schema forbids hyphens in every segment of that identifier — so the name that ends up in your FB package must stay hyphen-free.

After the rename, the FB package is fb.acme_worker.example.io — change example.io to your own domain by hand; the neops convention is a DNS-style namespace like fb.base.neops.io. It appears in several files (the function block modules, the workflow YAML in two places, and a test), so sweep with grep -r example.io and replace every hit — workflows pin FB identifiers exactly, and a partially renamed package surfaces only at runtime as “Function block not found”.

What You Get

acme_worker/fb/         example function blocks (hello_world, device_interface_report, connection_test)
workflows/              a workflow definition ready to publish to the engine
tests/                  FB tests (pytest, asyncio_mode=auto)
.env.dist               environment template — cp to .env
Dockerfile              runtime image; `make build-docker` builds it exactly as CI does
.github/workflows/      CI: lint, audit, type check, test, docker build
.vscode/                debug configurations for the worker and the tests

The example function blocks are graduated: hello_world.py is the smallest possible FB, device_interface_report.py reads entity data without opening a connection, and connection_test.py connects to a real device. Read them in that order as you work through the next pages.

Prefer starting from scratch?

You can also scaffold a minimal project by hand with uv:

uv init my-neops-worker
cd my-neops-worker
uv add neops_worker_sdk

Replace the generated pyproject.toml with a configuration tailored for neops development:

[project]
name = "my-neops-function-blocks"
version = "0.1.0"
description = "My first neops function blocks"
requires-python = ">=3.12"

dependencies = [
    "neops_worker_sdk",
]

[project.optional-dependencies]
test = [
    "pytest>=8.4.1",
    "pytest-asyncio>=1.1.0",
    "neops-remote-lab>=1.3.0",
]

[tool.ruff]
line-length = 120
target-version = "py312"

[tool.ruff.lint]
select = ["E", "F", "W", "I", "UP", "B"]

[tool.pytest.ini_options]
asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "function"

This gives you:

  • The neops_worker_sdk runtime dependency
  • Test dependencies (pytest, pytest-asyncio, neops-remote-lab) as optional extras
  • Sensible ruff and pytest configuration

Install everything, including test dependencies:

uv sync --all-extras

If you prefer pip over uv, create a virtual environment and install manually:

mkdir my-neops-worker && cd my-neops-worker
python -m venv .venv && source .venv/bin/activate
pip install neops_worker_sdk
pip install pytest pytest-asyncio  # for testing

Working on the SDK Itself

The neops-worker-sdk-py repository is primarily maintained internally, but pull requests are welcome. If you want to fix a bug or extend the SDK — rather than build function blocks on top of it — clone the repository itself and set up its development environment:

git clone https://github.com/zebbra/neops-worker-sdk-py.git
cd neops-worker-sdk-py
uv sync --extra test    # install all dependencies (dev + fb groups are defaults)
make lint test          # verify your environment works

The repository’s own README.md and AGENTS.md are the authoritative contributor references — they document the project structure, conventions, and the full verification gate to run before pushing (uv run pytest -q && make lint && make typeCheck-baseline && make audit). The rest of this guide assumes the worker path above, but the concepts apply either way.

Tooling

The boilerplate ships both tools below as development dependencies, wired to make lint and make typeCheck. If you scaffolded manually, add them as shown.

Linting & Formatting — Ruff

We use Ruff for linting and formatting. It is a single, extremely fast tool (written in Rust) that replaces flake8, isort, and Black.

If it is not in your project yet, add it as a development dependency so every contributor uses the same version:

uv add --dev ruff

Then run it from your project:

uv run ruff check .    # lint
uv run ruff format .   # format

Type Checking — Pyrefly

We use Pyrefly (by Meta) for type checking. It is a Rust-based type checker and language server that provides fast, accurate analysis for modern Python.

If needed, add it as a development dependency:

uv add --dev pyrefly

Then run it:

uv run pyrefly check .
Why typed Python matters

neops function blocks rely heavily on type annotations. Here is why that matters:

  • Auto-completion — your editor understands parameter and return types
  • Early bug detection — type checkers catch mismatches before runtime
  • Schema generation — Pydantic models (used for parameters and results) derive JSON schemas from type hints automatically

Compare:

def run(params, device):
    ...

vs.

async def run(self, params: EchoParams, context: WorkflowContext) -> FunctionBlockResult[EchoResult]:
    ...

The typed version gives your editor, your tests, and the neops platform everything they need to validate your code before it ever touches a device.

IDE Recommendations

The boilerplate already includes .vscode/settings.json (interpreter and pytest wiring) and .vscode/launch.json with two debug configurations: one that runs the live worker against your .env, and one for debugging tests.

Recommended extensions:

Extension ID Purpose
Python ms-python.python Core Python support
Pyrefly meta.pyrefly Type checking, auto-completion, go-to-definition
Ruff charliermarsh.ruff Fast linting and formatting

Pyrefly acts as a full language server — it provides inline type errors, hover information, and code navigation out of the box. It replaces Pylance for type analysis, so you only need one of the two.

Recommended settings (add to .vscode/settings.json):

{
    "[python]": {
        "editor.defaultFormatter": "charliermarsh.ruff",
        "editor.formatOnSave": true
    }
}

Alternative: Pylance

If you prefer Microsoft’s Pylance (ms-python.vscode-pylance), it works well too — add "python.analysis.typeCheckingMode": "basic" to your settings. Pyrefly disables Pylance by default when both are installed; keep only one active to avoid duplicate diagnostics.

PyCharm has excellent built-in Python support — type checking, refactoring, and debugging work out of the box with fewer plugins.

Recommended plugins:

Plugin Purpose
Pydantic Enhanced support for Pydantic models (auto-completion, validation)

Configuration tips:

  • Ruff integration: Go to Settings > Tools > External Tools, add a new tool with program uv, arguments run ruff check $FilePath$, and working directory $ProjectFileDir$. Add a second entry with arguments run ruff format $FilePath$ for formatting. Assign keyboard shortcuts or configure file watchers to run on save.
  • Pyrefly for type checking: Add an external tool with program uv, arguments run pyrefly check $FilePath$, and working directory $ProjectFileDir$. This gives you the same type analysis as CI, beyond PyCharm’s built-in inspector.

Environment Configuration

neops workers discover the platform and function blocks through environment variables. The boilerplate ships a documented template — copy it and adjust:

cp .env.dist .env

The two variables that matter first:

Variable Description
URL_BLACKBOARD URL of your neops instance’s blackboard API
DIR_FUNCTION_BLOCKS Directory where the worker discovers your function block modules (acme_worker/fb after the rename)

If you scaffolded manually, create a .env file in your project root instead:

URL_BLACKBOARD=https://your-neops-instance.example.com
DIR_FUNCTION_BLOCKS=./my_function_blocks

Tip

During local development with the test framework, these variables are not required – the test harness provides its own context.


Next: Write your first function block