Code Blocks are written in Python and support standard and select third-party libraries. They extend robot behavior beyond what drag-and-drop Routine steps can express — enabling custom motion logic, programmatic IO control, external device communication, and dynamic data computation.
A Code Block is a Routine step that lets you write and execute Python code inside an otherwise block-based Routine.
Standard drag-and-drop Routine steps cover the majority of robot programming tasks. Code Blocks exist to fill the gaps — they give you the flexibility to handle conditional logic, data computation, external communication, and other scenarios that cannot be expressed cleanly with blocks alone.
A Code Block is not a separate execution environment. It is simply another step in the Routine sequence. When the Routine reaches a Code Block, the Python code inside it runs, and then the Routine continues to the next step.
Consider using a Code Block when:
Code Blocks are designed to augment block-based programming, not replace it.
Avoid Code Blocks when:
Overusing Code Blocks can reduce the readability and maintainability of your Routine. If a block can do it, use a block.
Code Blocks can run in two modes: as a standard step, or as a Background Step.
Standard execution is synchronous — the Routine does
not advance to the next step until the Code Block has fully completed.
All statements run in order, top to bottom, making execution
predictable. Any operation that pauses execution — such as
time.sleep(), a polling loop, or a network request — also
pauses the Routine for the same duration.
Background Step execution is achieved by placing a Code Block inside a Run In Background step from the standard block-based programming palette. This allows the Code Block to run concurrently while the Routine continues to the next step — useful for tasks that need to run alongside robot motion or other steps without blocking the Routine.
If an unhandled error occurs inside a Code Block, the Code Block faults immediately and the Routine stops at that step. No downstream Routine steps execute.
Faults that occur inside a Code Block are not recoverable — the Routine must be restarted from the beginning. This applies regardless of whether the error would otherwise be considered recoverable in a standard block context. You cannot resume execution from the point of failure.
A Code Block failure is treated identically to any other faulted step
in a Routine. You can also trigger a fault intentionally using
routine.trigger_user_fault() — see the Triggering a
Fault section below.
Code Blocks can be copied and pasted between Routines. If your code references named variables or saved spaces, make sure those also exist in the destination Routine — otherwise the block may fault at runtime.
The Code Block editor has three main areas.
Text Editor
Where you write Python code. The editor supports syntax highlighting and inline validation as you type.
Linter / Debug Panel
Displays syntax errors and runtime warnings before you execute the block. Check this panel to catch issues early.
Fx Button
The Fx button is located in the top right of the text editor, next to the Save button. Clicking it opens a library of pre-built functions and expressions you can insert directly into your code. Useful for quickly referencing available runtime objects and their methods.
Code Blocks support the full standard Python syntax, including:
if, for,
while, try/except, etc.)print, len,
range, int, float,
str, etc.)
The following third-party packages are available without installation:
| Package | Purpose |
|---|---|
python-dateutil |
Date and time parsing and arithmetic |
urllib3 |
HTTP client library |
pymodbus |
Modbus TCP and RTU communication |
pyserial |
Serial port communication |
orjson |
Fast JSON serialization and deserialization |
Four objects give you access to the robot, routine state, equipment,
and execution context from within a Code Block: routine,
robot, equipment, and
context.
These objects are not automatically imported — they are imported and injected by the default Code Block function, which is pre-populated in every new Code Block. When you open a new Code Block you will see this template already in place:
from typing import Any
from standardbots import Context, Equipment, Robot, Routine # objects are imported here
def code_block(context: Context, routine: Routine, robot: Robot, equipment: Equipment, **kwargs: Any):
pass # your code goes hereThe line
from standardbots import Context, Equipment, Robot, Routine
is what makes the four objects available. Do not remove this import or
the function signature — your code goes inside the
code_block function in place of pass.
The **kwargs: Any in the function signature captures any
additional arguments the runtime may pass in future versions. You do not
need to use or reference kwargs in your code — it is
included in the template for forward compatibility and can be safely
ignored.
| Object | Description |
|---|---|
routine |
Access to the active Routine’s variables, state, spaces, and flow control. |
robot |
Interface for robot motion, position queries, IO, and fault recovery. |
equipment |
Access to configured tools and end-effectors such as grippers and sensors. |
context |
Read-only metadata about the current execution environment. |
All four objects are scoped to the active Routine instance. They exist only while the Routine is executing and are not accessible outside a Code Block.
routine
ObjectThe routine object represents the currently executing
Routine. It is your primary interface for reading and writing data
shared across Routine steps.
Routine variables are key-value pairs accessible by name. They persist across all steps in the Routine — a variable written in one step is available to all downstream steps, whether those are Code Blocks or standard blocks.
| Usage | Syntax | Notes |
|---|---|---|
| Read a variable | routine.variables["name"] |
Returns the current value. Faults if the variable does not exist. |
| Write a variable | routine.variables["name"] = value |
Creates the variable if it doesn’t exist; overwrites if it does. |
| Shorthand alias | routine.vars["name"] |
Identical to routine.variables["name"]. |
Routine variables are stored as strings. When reading a numeric value, cast it explicitly:
Naming conventions: Variable names are case-sensitive. Spaces are allowed (e.g.
"Cycle Count"). Avoid special characters other than spaces, underscores, and hyphens to ensure compatibility across all Routine steps. Names should be consistent in casing throughout the Routine — mixing"cycleCount"and"Cycle Count"will create two separate variables.
count = int(routine.variables["Cycle Count"])
speed = float(routine.variables["Target Speed"])
Example — read, compute, and write back:
count = int(routine.variables["Cycle Count"])
new_count = count + 1
routine.variables["Cycle Count"] = str(new_count)
print(f"Completed cycle {new_count}")
| Function | Syntax | Description |
|---|---|---|
| Get state | routine.get_state() |
Returns the current state of the Routine. |
| Stop routine | routine.stop_routine() |
Immediately stops the executing Routine. |
Example:
state = routine.get_state()
print(state)
| Function | Syntax | Description |
|---|---|---|
| List spaces | routine.list_spaces(exclude_global_spaces=False) |
Returns all saved spaces. Pass True to exclude global
spaces. |
| List planes | routine.list_planes() |
Returns all planes associated with the Routine. |
exclude_global_spaces ParameterControls whether globally defined spaces are included in the returned list.
| Value | Behavior |
|---|---|
exclude_global_spaces=False |
Returns all spaces, including global spaces shared across Routines. |
exclude_global_spaces=True |
Returns only spaces local to the current Routine, excluding any global spaces. |
Example — list all spaces:
spaces = routine.list_spaces(exclude_global_spaces=False)
routine.vars["Spaces Info"] = str(spaces)
print(str(spaces))Example — list local spaces only:
local_spaces = routine.list_spaces(exclude_global_spaces=True)
print(str(local_spaces))
Code Blocks support standard Python try/except blocks.
Wrapping code in a try/except lets you catch errors
gracefully and decide how to respond — logging the issue, writing a
fallback value, or triggering a controlled fault — rather than letting
an unhandled exception stop the Routine unexpectedly.
Example — catch a missing variable:
try:
count = int(routine.variables["Cycle Count"])
except KeyError:
routine.trigger_user_fault(message="Variable 'Cycle Count' not found", isRecoverable=False)Example — catch a communication error and continue:
try:
with robot.io.modbus.open_tcp("192.168.1.100", 502) as client:
result = client.read_holding_registers(address=0, count=1)
routine.variables["Sensor Value"] = str(result.registers[0])
except Exception as e:
print(f"Modbus read failed: {e}")
routine.variables["Sensor Value"] = "0" # fallback valueExample — catch any unexpected error and trigger a recoverable fault:
try:
# your logic here
pass
except Exception as e:
routine.trigger_user_fault(message=f"Unexpected error: {e}", isRecoverable=True)
You can halt the Routine intentionally with a descriptive error
message using trigger_user_fault.
| Parameter | Type | Description |
|---|---|---|
message |
str |
The error message displayed when the fault occurs. |
isRecoverable |
bool |
If True, the operator can recover and resume. If
False, a full restart is required. |
isRecoverable
ParameterControls whether the operator can recover from the fault and continue the Routine, or whether a full restart is required.
| Value | Behavior |
|---|---|
isRecoverable=True |
The Routine halts and displays the fault message. The operator can acknowledge the fault and resume from where it stopped. |
isRecoverable=False |
The Routine halts and displays the fault message. A full restart of the Routine is required to continue. |
Example — recoverable fault:
routine.trigger_user_fault(message='testfault', isRecoverable=True)Example — non-recoverable fault:
routine.trigger_user_fault(message='testfault', isRecoverable=False)robot
ObjectThe robot object provides programmatic access to the
robot — its motion, current position, IO, and fault recovery. It
controls the same robot targeted by the Routine’s block-based steps.
| Function | Description |
|---|---|
robot.move(position, orientation, ...) |
Move the robot to a given position and orientation. The robot plans and executes the motion to reach the target pose. |
robot.move_tooltip(position, orientation, ...) |
Move the tooltip (end-effector tip) to a position. Use this when the target position should be interpreted relative to the tooltip rather than the robot flange. |
Example — move to a Cartesian position:
target = robot.cartesian_pose(x=300, y=0, z=400, rx=0, ry=180, rz=0)
robot.move(target)
Use these helpers to construct and manipulate poses.
| Function | Description |
|---|---|
robot.cartesian_pose(x, y, z, rx, ry, rz) |
Create a Cartesian pose from position (mm) and Euler angles (degrees). |
robot.joint_pose(j0, j1, j2, j3, j4, j5) |
Create a joint-space pose from six joint angles (degrees). |
robot.cartesian_offset(x, y, z, rx, ry, rz) |
Create an offset relative to the current Cartesian pose. |
robot.poses_addition(pose1, pose2) |
Combine two Cartesian poses additively. |
robot.poses_subtraction(pose1, pose2) |
Subtract one Cartesian pose from another. |
robot.pose_distance(pose1, pose2) |
Compute the distance between two Cartesian poses. |
Example — apply a Z offset from the current position:
offset = robot.cartesian_offset(x=0, y=0, z=50, rx=0, ry=0, rz=0)
robot.move(offset)
| Function | Description |
|---|---|
robot.get_position() |
Get the current arm position. |
robot.joints_position() |
Get current joint angles for all six joints. |
robot.tooltip_position() |
Get the current tooltip (end-effector tip) position. |
robot.flange_position() |
Get the current flange position. |
Example:
current_pos = robot.get_position()
print(f"Current position: {current_pos}")
| Function | Description |
|---|---|
robot.brake() |
Engage the robot’s brakes. |
robot.unbrake() |
Disengage the robot’s brakes. |
robot.kind | Returns
"live" or "simulated" depending on execution
mode. |
The robot’s digital IO can be read and written at the object level.
| Function | Description |
|---|---|
robot.get_io_status() |
Returns a snapshot of the current digital IO state. |
robot.change_io_status(io_map) |
Updates IO outputs using a key-value map. |
For per-pin access, see IO Interfaces below.
equipment
ObjectThe equipment object provides access to tools and
end-effectors configured for the Routine, such as grippers and
sensors.
| Function | Description |
|---|---|
equipment.get_gripper_config() |
Returns the current gripper configuration. |
equipment.actuate_gripper(command) |
Send a command to open, close, or position the gripper. |
The most common commands are "open" and
"close". Additional commands (such as specific position or
force targets) depend on the gripper model configured for the Routine —
check your equipment configuration for the full list of supported
commands.
Example — open and close a gripper:
import time
equipment.actuate_gripper("open")
time.sleep(1.0)
equipment.actuate_gripper("close")context
ObjectThe context object provides read-only metadata about the
current execution environment.
| Property | Description |
|---|---|
context.kind |
Describes the type of the current block — either
"code-block-step" (a full Routine step) or
"expression" (an inline expression context). |
context.robot_kind |
Indicates whether the robot is "live" (connected to a
real robot) or "simulated". Use this to branch behavior
between test and production environments. |
context._sdk |
Internal access to the raw SDK client. This property is intentionally undocumented and unsupported — use it only if directed to by Standard Bots support. |
Example — skip hardware calls in simulation:
if context.robot_kind == "simulated":
print("Simulation mode — skipping hardware call")
else:
# Execute live hardware interaction here
passIO is accessed through robot.io, which exposes three
sub-interfaces: digital IO, serial, and Modbus.
cb_digital)Digital inputs and outputs are accessed by pin number (1–16).
| Operation | Syntax | Returns |
|---|---|---|
| Read a digital input | robot.io.cb_digital.i["1"] |
"high" or "low" |
| Set a digital output | robot.io.cb_digital.o["2"] = "high" |
— |
Example — wait for a sensor signal:
import time
while robot.io.cb_digital.i["3"] == "low":
time.sleep(0.1)
print("Sensor triggered")Example — activate an output:
robot.io.cb_digital.o["5"] = "high"
Use robot.io.serial.open() as a context manager for raw
serial communication via pyserial.
| Function | Description |
|---|---|
robot.io.serial.open(port, baudrate, **kwargs) |
Opens a serial connection. All standard pyserial kwargs
are supported. |
Example:
with robot.io.serial.open("/dev/ttyUSB0", 9600) as ser:
ser.write(b"PING\n")
response = ser.readline()
print(f"Response: {response}")
Modbus communication is available via robot.io.modbus,
which wraps pymodbus. Both methods are used as context
managers.
| Function | Description |
|---|---|
robot.io.modbus.open_tcp(host, port, **kwargs) |
Connect to a Modbus TCP server. |
robot.io.modbus.open_rtu(port, baudrate, **kwargs) |
Connect to a Modbus RTU (serial) device. |
Example — read a holding register over TCP:
host = str(routine.variables["Device IP"])
with robot.io.modbus.open_tcp(host, 502) as client:
result = client.read_holding_registers(address=0, count=2)
value = client.convert_from_registers(result.registers, data_type=client.DATATYPE.INT32)
routine.variables["Sensor Value"] = str(value)
print(f"Register value: {value}")Example — write a register over TCP:
host = str(routine.variables["Device IP"])
with robot.io.modbus.open_tcp(host, 502) as client:
client.write_register(address=10, value=1)This section shows practical, copy-ready patterns for the most frequent Code Block use cases. These are starting points — not complete programs. Adapt variable names, pin numbers, addresses, and logic to match your specific Routine and hardware configuration. For full API details on any function used here, refer to the relevant object section above.
See also: The
routine Object → Reading and Writing Variables
# Read a string variable
label = str(routine.variables["Part Label"])
# Read numeric variables (always cast from string)
speed = float(routine.variables["Target Speed"])
count = int(routine.variables["Cycle Count"])
# Compute and write back a result
new_count = count + 1
routine.variables["Cycle Count"] = str(new_count)
print(f"Completed cycle {new_count} at speed {speed}")
See also: The
routine Object → Spaces and Planes
spaces = routine.list_spaces(exclude_global_spaces=False)
target_space = next((s for s in spaces if s["name"] == "Pick Position"), None)
if target_space is None:
routine.trigger_user_fault(message="Space 'Pick Position' not found", isRecoverable=False)
robot.move(target_space)
See also: The
robot Object → Motion Control, Pose Utilities
# Move to an absolute Cartesian position
target = robot.cartesian_pose(x=400, y=-150, z=300, rx=0, ry=180, rz=90)
robot.move(target)
# Move with a Z offset from the current position
offset = robot.cartesian_offset(x=0, y=0, z=50, rx=0, ry=0, rz=0)
robot.move(offset)
See also: The
equipment Object
import time
equipment.actuate_gripper("open")
time.sleep(0.5)
equipment.actuate_gripper("close")
time.sleep(0.5)
See also: IO Interfaces → Digital IO
import time
# Wait for input pin 1 to go high (e.g. a part-present sensor)
while robot.io.cb_digital.i["1"] == "low":
time.sleep(0.05)
# Pulse output pin 2 (e.g. a conveyor start signal)
robot.io.cb_digital.o["2"] = "high"
time.sleep(1.0)
robot.io.cb_digital.o["2"] = "low"
See also: IO Interfaces → Modbus TCP and RTU
import time
device_ip = str(routine.variables["Device IP"])
with robot.io.modbus.open_tcp(device_ip, 502) as client:
time.sleep(0.5)
# Send a start command
client.write_register(address=10, value=1)
# Poll until the device reports completion
status = 1
while status != 0:
result = client.read_holding_registers(address=10, count=1)
status = result.registers[0]
time.sleep(0.1)
# Read a position feedback register
raw = client.read_holding_registers(address=0, count=2)
position = client.convert_from_registers(raw.registers, data_type=client.DATATYPE.INT32)
routine.variables["Device Position"] = str(float(position) / 1000.0)
See also: IO Interfaces → Serial Communication
port = str(routine.variables["Serial Port"])
with robot.io.serial.open(port, 115200, timeout=1.0) as ser:
ser.write(b"STATUS\r\n")
response = ser.readline().decode("utf-8").strip()
routine.variables["Device Status"] = response
print(f"Device responded: {response}")
sensor_value = float(routine.variables["Sensor Reading"])
if sensor_value < 0 or sensor_value > 100:
routine.trigger_user_fault(
message=f"Sensor value {sensor_value} is out of acceptable range (0–100)",
isRecoverable=True
)
print() output is visible in the DevTools
Console in Chrome while connected to the robot. Navigate to
DevTools → Console to see output in real time.
current_pos = robot.get_position()
print(f"Robot is at: {current_pos}")
loop_count = int(routine.variables.get("Loop Count", 0))
print(f"Iteration: {loop_count}")
if context.robot_kind == "simulated":
print("Running in simulation — hardware calls will be skipped")
routine.variables["Sensor Value"] = "42"
else:
with robot.io.modbus.open_tcp("192.168.1.100", 502) as client:
raw = client.read_holding_registers(address=5, count=1)
routine.variables["Sensor Value"] = str(raw.registers[0])Print liberally during development.
print() output is visible in Chrome’s DevTools
Console. Use it freely while developing — remove or reduce it
once the logic is validated.
Use context.robot_kind for testability.
Always add a simulation branch when interacting with hardware. This lets
you run and validate your Routine safely in the simulator before
connecting to the robot.
Test Modbus and serial connections in simulation
first. Use context.robot_kind == "simulated" to
mock hardware responses during development, then switch to live once the
logic is validated.
Keep Code Blocks focused. A Code Block that does one thing is easier to debug and maintain than one that does everything. If a block is getting long, consider splitting logic across multiple blocks.
Copy and paste Code Blocks carefully. When moving a Code Block to another Routine, verify that all referenced variables and saved spaces also exist in the destination Routine, or the block will fault on its first run.
Cast your variables. Routine variables are always
stored as strings. Always cast them before performing numeric
operations: int(routine.variables["count"]),
float(routine.variables["speed"]).
Check for variable existence. If a variable might
not exist yet, use Python’s dict.get() with a default:
routine.variables.get("My Var", "default value").
Variables written in a Code Block are available immediately
downstream. Any variable set with
routine.variables["name"] = value is available to the very
next step in the Routine, whether that’s another Code Block or a
standard block.
Use recoverable faults as checkpoints.
trigger_user_fault(isRecoverable=True) pauses the Routine
and waits for operator acknowledgment before continuing. It is useful
for adding inspection or confirmation points to a Routine.
Wrap hardware calls in try/except. Communication failures with Modbus or serial devices can occur at runtime. Catching exceptions allows you to handle them gracefully rather than faulting unexpectedly. See the Error Handling section for patterns.
Avoid tight polling loops. When polling for a
condition, always include a time.sleep() inside the loop to
prevent excessive CPU usage and allow other system processes to run.