This library provides an SDK for the Wandelbots NOVA API.
The SDK will help you to build your own apps and services using Python on top of Wandelbots NOVA and makes programming a robot as easy as possible.
417768496-f6157e4b-eea8-4b96-b302-1f3864ae44a9.webm
- Overview
- Prerequisites
- Quickstart
- Installation
- Using the SDK
- Wandelscript
- NOVAx
- Development
- Release process
- Additional resources
Wandelbots NOVA OS is a robot-agnostic operating system that enables developers to plan, program, control, and operate fleets of six-axis industrial robots through a unified API, across all major robot brands. It integrates modern development tools like Python and JavaScript APIs with AI-based control and motion planning, allowing developers to build automation tasks such as gluing, grinding, welding, and palletizing without needing to account for hardware differences. The software offers a powerful set of tools that support the creation of custom automation solutions throughout the entire automation lifecycle.
- A running NOVA instance (Get a Wandelbots NOVA account on wandelbots.com)
- Valid NOVA API credentials
- Python >=3.10
- Install the SDK using
pipor set up a localuvproject with extras for visualization. Refer to the Installation section for both options. - Copy
.env.templateto.envand fill in the base URL and access token for your NOVA deployment. Details are covered in Configure environment variables. - Run an example to validate the setup, e.g.
uv run python examples/basic.py. Install the rerun extras and executeuv run download-modelsif you want interactive 3D visualization out of the box.
Install the library using pip:
pip install wandelbots-novaInstall uv on your system.
Initialize a new uv project with the following command.
uv initInstall the library with the nova-rerun-bridge extra to use the visualization tool rerun.
See extension README.md for further details.
uv add wandelbots-nova --extra nova-rerun-bridgeDownload the robot models to visualize them in the rerun viewer.
uv run download-modelsCopy the provided .env.template file and rename it to .env:
cp .env.template .envOpen the .env file in a text editor and fill in the values. Here's what each variable does:
| Variable | Description | Required | Default | Example |
|---|---|---|---|---|
NOVA_API |
Base URL or hostname of the Wandelbots NOVA server instance | Yes | None | https://nova.example.com or http://172.0.0.1 |
NOVA_ACCESS_TOKEN |
Pre-obtained access token for Wandelbots NOVA (cloud or self-hosted deployments) | Yes* | None | eyJhbGciOi... |
Note:
NOVA_APIis mandatory in every deployment. Always point it to the NOVA base URL you are targeting.NOVA_ACCESS_TOKENis the supported authentication mechanism. It is mandatory for the Wandelbots Cloud environment; for self-hosted deployments generate and supply a token with the required permissions.- Username/password authentication (
NOVA_USERNAME/NOVA_PASSWORD) is deprecated and no longer supported.
Import the library in your code to get started.
from nova import NovaYou can access the automatically generated NOVA API client using the api module.
from nova import apiCheck out the basic and plan_and_execute examples to learn how to use the library.
Curated examples in this repository showcase typical SDK workflows:
import nova
from nova import Nova
from nova.program import ProgramPreconditions
from nova.cell import virtual_controller
@nova.program(
preconditions=ProgramPreconditions(
controllers=[
virtual_controller(
name="ur10",
manufacturer=api.models.Manufacturer.UNIVERSALROBOTS,
type=api.models.VirtualControllerTypes.UNIVERSALROBOTS_MINUS_UR10E,
),
virtual_controller(
name="kuka",
manufacturer=api.models.Manufacturer.KUKA,
type=api.models.VirtualControllerTypes.KUKA_MINUS_KR16_R1610_2,
),
],
)
)
async def main():
async with Nova() as nova:
cell = nova.cell()
ur10 = await cell.controller("ur10")
kuka = await cell.controller("kuka")
async with ur10[0] as motion_group:
tcp = "Flange"
home_joints = await motion_group.joints()
current_pose = await motion_group.tcp_pose(tcp)- Basic Point-to-Point movement
import nova
from nova import Nova
from nova.actions import cartesian_ptp, joint_ptp
from nova.types import Pose
@nova.program()
async def main():
async with Nova() as nova:
actions = [
joint_ptp(home_joints),
cartesian_ptp(current_pose @ Pose((100, 0, 0, 0, 0, 0))), # Move 100mm in X
joint_ptp(home_joints)
]
trajectory = await motion_group.plan(actions, tcp)- Collision-free movement
from nova.actions import collision_free
from nova.types import Pose, MotionSettings
from math import pi
actions = [
collision_free(
target=Pose((-500, -400, 200, pi, 0, 0)),
collision_scene=collision_scene,
settings=MotionSettings(tcp_velocity_limit=30)
)
]wandelbots-nova_simple_welding.mp4
- Multiple robot coordination
import asyncio
async def move_robots():
async with ur10[0] as ur_mg, kuka[0] as kuka_mg:
await asyncio.gather(
move_robot(ur_mg, "Flange"),
move_robot(kuka_mg, "Flange")
)More information in move_multiple_robots.
- Synchronous execution start of multiple motion groups
Explore coordinated execution with multi_motion_group.
- Input/Output control
from nova.actions import io_write, joint_ptp, cartesian_ptp
actions = [
joint_ptp(home_joints),
io_write(key="digital_out[0]", value=False), # Set digital output
cartesian_ptp(target_pose),
joint_ptp(home_joints)
]- 3D visualization with rerun
# Basic 3D visualization (default)
@nova.program(
viewer=nova.viewers.Rerun()
)
async def main():
async with Nova() as nova:
cell = nova.cell()
controller = await cell.controller("robot1")
async with controller[0] as motion_group:
actions = [cartesian_ptp(target_pose)]
trajectory = await motion_group.plan(actions, tcp)
# Trajectory is automatically visualized in Rerun# Advanced visualization with detailed panels and tool models
@nova.program(
viewer=nova.viewers.Rerun(
show_details=True, # Show detailed analysis panels
show_safety_zones=True, # Show robot safety zones
show_collision_link_chain=True, # Show collision geometry
show_collision_tool=True, # Show TCP tool collision geometry
tcp_tools={
"vacuum": "assets/vacuum_cup.stl",
"gripper": "assets/parallel_gripper.stl"
}
)
)Note: Install rerun extras to enable visualization
- Custom TCPs (Tool Center Points)
import json
import nova
from nova import Nova
from nova.api import models
from nova.actions import cartesian_ptp
from nova.types import Pose
# Define TCP configuration
tcp_config = {
"id": "vacuum_gripper",
"readable_name": "Vacuum Gripper",
"position": {"x": 0, "y": 0, "z": 100}, # 100mm in Z direction
"rotation": {"angles": [0, 0, 0], "type": "EULER_ANGLES_EXTRINSIC_XYZ"}
}
@nova.program(
name="Add TCP",
preconditions=ProgramPreconditions(
controllers=[
virtual_controller(
name="robot1",
manufacturer=api.models.Manufacturer.UNIVERSALROBOTS,
type=api.models.VirtualControllerTypes.UNIVERSALROBOTS_MINUS_UR10E,
),
],
cleanup_controllers=False,
),
)
async def setup_tcp():
async with Nova() as nova:
cell = nova.cell()
controller = await cell.controller("robot1")
# Add TCP to virtual robot
tcp_config_obj = models.RobotTcp.from_json(json.dumps(tcp_config))
await nova._api_client.virtual_robot_setup_api.add_virtual_robot_tcp(
cell.cell_id,
controller.controller_id,
motion_group_idx=0,
tcp_config_obj
)
# Use the new TCP
async with controller[0] as motion_group:
current_pose = await motion_group.tcp_pose("vacuum_gripper")
# Plan motions using the new TCP
actions = [cartesian_ptp(current_pose @ Pose((100, 0, 0, 0, 0, 0)))]
trajectory = await motion_group.plan(actions, "vacuum_gripper")
- Common coordinate systems for multiple robots
from math import pi
import asyncio
import nova
from nova.api.models import CoordinateSystem, Vector3d, RotationAngles, RotationAngleTypes
from nova.actions import cartesian_ptp
from nova.types import Pose
@nova.program(
preconditions=ProgramPreconditions(
controllers=[
virtual_controller(
name="ur10",
manufacturer=api.models.Manufacturer.UNIVERSALROBOTS,
type=api.models.VirtualControllerTypes.UNIVERSALROBOTS_MINUS_UR10E,
),
virtual_controller(
name="kuka",
manufacturer=api.models.Manufacturer.KUKA,
type=api.models.VirtualControllerTypes.KUKA_MINUS_KR16_R1610_2,
),
],
cleanup_controllers=False,
),
)
async def setup_coordinated_robots():
async with Nova() as nova:
cell = nova.cell()
# Setup robots
robot1 = await cell.controller("ur10")
robot2 = await cell.controller("kuka")
# Define common world coordinate system
world_mounting = CoordinateSystem(
coordinate_system="world",
name="mounting",
reference_uid="",
position=Vector3d(x=0, y=0, z=0),
rotation=RotationAngles(
angles=[0, 0, 0],
type=RotationAngleTypes.EULER_ANGLES_EXTRINSIC_XYZ
)
)
# Position robots relative to world coordinates
await nova._api_client.virtual_robot_setup_api.set_virtual_robot_mounting(
cell="cell",
controller=robot1.controller_id,
id=0, # motion_group_id
coordinate_system=CoordinateSystem(
coordinate_system="world",
name="robot1_mount",
reference_uid="",
position=Vector3d(x=500, y=0, z=0), # Robot 1 at x=500mm
rotation=RotationAngles(
angles=[0, 0, 0],
type=RotationAngleTypes.EULER_ANGLES_EXTRINSIC_XYZ
)
)
)
await nova._api_client.virtual_robot_setup_api.set_virtual_robot_mounting(
cell="cell",
controller=robot2.controller_id,
id=0, # motion_group_id
coordinate_system=CoordinateSystem(
coordinate_system="world",
name="robot2_mount",
reference_uid="",
position=Vector3d(x=-500, y=0, z=0), # Robot 2 at x=-500mm
rotation=RotationAngles(
angles=[0, 0, pi], # Rotated 180° around Z
type=RotationAngleTypes.EULER_ANGLES_EXTRINSIC_XYZ
)
)
)
# Now both robots can work in the same coordinate system
async with robot1[0] as mg1, robot2[0] as mg2:
# Movements will be relative to world coordinates
await asyncio.gather(
mg1.plan([cartesian_ptp(Pose((0, 100, 0, 0, 0, 0)))], "tcp1"),
mg2.plan([cartesian_ptp(Pose((0, -100, 0, 0, 0, 0)))], "tcp2")
)
Wandelscript is a domain-specific language for programming robots. It is a declarative language that allows you to describe the robot's behavior in a high-level way. Wandelscript is suited to get yourself familiar with robot programming.
uv add wandelbots-nova --extra wandelscriptHere is a simple example of a Wandelscript program:
robot = get_controller("controller")[0]
tcp("Flange")
home = read(robot, "pose")
sync
# Set the velocity of the robot to 200 mm/s
velocity(200)
for i = 0..3:
move via ptp() to home
# Move to a pose concatenating the home pose
move via line() to (50, 20, 30, 0, 0, 0) :: home
move via line() to (100, 20, 30, 0, 0, 0) :: home
move via line() to (50, 20, 30, 0, 0, 0) :: home
move via ptp() to homeTo get started, use the Quickstart. For implementation details or contributing to Wandelscript, refer to the Wandelscript readme.
NOVAx is an app framework for building server applications on top of Wandelbots NOVA. It provides common core concepts like the handling of programs and their execution.
You can create a new NOVAx app using the NOVA CLI generator:
nova app create "your-nova-app" -g python_appFor more information on using NOVAx see the README. Explore this example to use the NOVAx entry point.
Important: When using NOVAx, you must import the actual program functions from their respective Python files. Only importing the program files won't suffice. This ensures proper function registration and execution within the NOVAx runtime environment.
To install development dependencies, run
uv sync --extra "nova-rerun-bridge"uv run ruff format
uv run ruff check --select I --fixdocker run --rm -it -v $(pwd):/data cytopia/yamllint -d .yamllint .When working with feature branches or forks, it can be helpful to test the library as a dependency in other projects before merging. You can specify custom sources in your pyproject.toml to pull the library from a specific branch:
Using PEP 621-style table syntax:
wandelbots-nova = { git = "https://github.com/wandelbotsgmbh/wandelbots-nova.git", branch = "fix/http-prefix" }Using PEP 508 direct URL syntax:
wandelbots-nova @ git+https://github.com/wandelbotsgmbh/wandelbots-nova.git@fix/http-prefix| Branch | Purpose | Published to | Example version |
|---|---|---|---|
main |
Stable releases (semantic versioning vX.Y.Z) | PyPI (pip install wandelbots-nova) |
v1.13.0 |
release/* |
LTS-releases, pre-releases or hotfixes for older lines | PyPI (labeled with release suffix) | v1.8.7-release-1.x |
| any other | Development builds | GitHub actions (not published to PyPI) | e4c8af0647839... |
Merging into main triggers the release workflow:
semantic-releaseanalyzes commit messages and bumps the version automatically.- A source distribution and wheel are built and uploaded to PyPI.
- A GitHub release is created (or updated) with the release assets.
If you're on older major versions or under a special LTS contract:
- Use (or create) a branch like
release/1.x,release/customer-foo, etc. - Every commit to these branches triggers the same workflow as on
main. - Versions include the branch name to prevent collisions, e.g.
v1.8.7-release-1.x
Need a temporary test build? Use GitHub actions:
-
Go to the actions tab.
-
Find Nova SDK: Build dev wheel and click
Run workflow. -
Select a branch and trigger the job.
-
After completion, open the Installation step to copy the ready-to-use
pip installcommand:pip install "wandelbots-nova @ git+https://github.com/wandelbotsgmbh/wandelbots-nova.git@<commit>"
- Examples covering basic to advanced SDK scenarios
- Technical wiki with architecture notes and troubleshooting tips
- Official documentation for platform concepts and API guides
- Code documentation generated from the latest SDK build