Authoring a Task Program in Python#
This tutorial builds, validates, and compiles an Embodied Task Program entirely in Python. It focuses on the provider-independent language: no simulator, Gym environment, robot asset, or YAML file is required.
The result is a compiled semantic workflow, not robot commands. Continue with Creating a Modular Environment and Configure and Run an Embodied Task Program when you are ready to bind the same kind of program to a simulation environment. Use Atomic actions instead when application code should directly plan or execute individual Atomic Skills.
What you will build#
The example declares one scene object and a typed Pick-and-Place workflow:
Repeat (3 times)
└── Segment: move_cube
├── Pick cube
├── Place cube at the next cyclic target
├── Wait for cube to become stable
└── Validate cube is near the selected target
Two target poses are selected cyclically, so the three repetitions use target
indices 0, 1, and 0. Compilation expands those occurrences into
stable segments and call indices without observing live scene state.
The code#
The complete runnable example is
scripts/tutorials/task_program/build_and_compile.py:
Code for build_and_compile.py
1# ----------------------------------------------------------------------------
2# Copyright (c) 2021-2026 DexForce Technology Co., Ltd.
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15# ----------------------------------------------------------------------------
16
17"""Build and compile a provider-independent Task Program in Python."""
18
19from __future__ import annotations
20
21from embodichain.lab.task_program import (
22 CompiledTaskProgram,
23 CyclicPoseTargetCfg,
24 InvokeCfg,
25 ObjectNearTargetValidatorCfg,
26 PickCfg,
27 PlaceCfg,
28 PoseCfg,
29 RepeatCfg,
30 SegmentCfg,
31 SequenceCfg,
32 TargetRefCfg,
33 TaskProgramCfg,
34 TaskProgramCompiler,
35 TaskProgramIntegrationCfg,
36 WaitStablePostCfg,
37)
38from embodichain.lab.task_program.semantics import (
39 SceneEntityManifest,
40 SceneManifest,
41 SceneObjectRef,
42)
43
44__all__ = [
45 "build_program",
46 "build_scene_manifest",
47 "main",
48 "print_compiled_program",
49]
50
51
52def build_scene_manifest() -> SceneManifest:
53 """Declare the provider-independent scene identities used by the program."""
54 return SceneManifest(
55 entries=(
56 SceneEntityManifest(
57 ref=SceneObjectRef("cube"),
58 semantic_type="cube",
59 ),
60 )
61 )
62
63
64def build_program() -> TaskProgramCfg:
65 """Build a typed Pick-and-Place Task Program without YAML or JSON."""
66 drop_poses = CyclicPoseTargetCfg(
67 values=(
68 PoseCfg(
69 position=(0.40, -0.20, 0.10),
70 quaternion_wxyz=(1.0, 0.0, 0.0, 0.0),
71 ),
72 PoseCfg(
73 position=(0.40, 0.20, 0.10),
74 quaternion_wxyz=(1.0, 0.0, 0.0, 0.0),
75 ),
76 )
77 )
78 move_cube = SegmentCfg(
79 name="move_cube",
80 steps=SequenceCfg(
81 items=(
82 InvokeCfg(
83 call=PickCfg(
84 object="cube",
85 resources={"primary": "manipulator"},
86 )
87 ),
88 InvokeCfg(
89 call=PlaceCfg(
90 object="cube",
91 at=TargetRefCfg(target="drop_pose"),
92 resources={"primary": "manipulator"},
93 )
94 ),
95 )
96 ),
97 post=(WaitStablePostCfg(entity="cube"),),
98 validators=(
99 ObjectNearTargetValidatorCfg(
100 object="cube",
101 target="drop_pose",
102 position_tolerance=0.03,
103 ),
104 ),
105 )
106 return TaskProgramCfg(
107 program_id="python_pick_and_place",
108 integration=TaskProgramIntegrationCfg(
109 robot_profile="tutorial_robot",
110 scene_registry="tutorial_scene",
111 runtime_preset="trajectory",
112 ),
113 targets={"drop_pose": drop_poses},
114 program=RepeatCfg(count=3, body=move_cube),
115 )
116
117
118def print_compiled_program(compiled: CompiledTaskProgram) -> None:
119 """Print the deterministic expansion produced by the compiler."""
120 print(f"Program: {compiled.program_id}")
121 print(f"Segments: {compiled.segment_count}")
122 for segment in compiled:
123 repeat = segment.repeat_frames[-1]
124 call_names = " -> ".join(type(item.call).__name__ for item in segment.calls)
125 target = segment.calls[-1].target_selections[0]
126 print(
127 f"[{segment.segment_index}] {segment.name} "
128 f"(repeat {repeat.iteration_index + 1}/{repeat.count}): "
129 f"{call_names}; {target.target_id}[{target.value_index}]"
130 )
131
132
133def main() -> None:
134 """Compile and inspect the tutorial Task Program."""
135 scene_manifest = build_scene_manifest()
136 program = build_program()
137 compiled = TaskProgramCompiler(scene_manifest).compile(program)
138 print_compiled_program(compiled)
139
140
141if __name__ == "__main__":
142 main()
Run it from the repository root:
python scripts/tutorials/task_program/build_and_compile.py
The output shows the deterministic expansion:
Program: python_pick_and_place
Segments: 3
[0] move_cube (repeat 1/3): Pick -> Place; drop_pose[0]
[1] move_cube (repeat 2/3): Pick -> Place; drop_pose[1]
[2] move_cube (repeat 3/3): Pick -> Place; drop_pose[0]
Declare provider-independent scene identities#
The compiler needs canonical identities and their semantic types, but it does
not need simulator objects or state providers. A
SceneManifest is the static
catalog for that boundary:
def build_scene_manifest() -> SceneManifest:
"""Declare the provider-independent scene identities used by the program."""
return SceneManifest(
entries=(
SceneEntityManifest(
ref=SceneObjectRef("cube"),
semantic_type="cube",
),
)
)
SceneObjectRef("cube") establishes that cube is an object rather than
an articulation, link, or affordance. The compiler uses this typed identity to
reject invalid or unknown references before a live environment is created.
Build the typed program tree#
Python authoring uses the same declarative schema as JSON or YAML. Constructors
such as PickCfg, PlaceCfg, SequenceCfg, and
RepeatCfg validate their own fields, so discriminator strings such as
kind: pick do not need to be written manually:
def build_program() -> TaskProgramCfg:
"""Build a typed Pick-and-Place Task Program without YAML or JSON."""
drop_poses = CyclicPoseTargetCfg(
values=(
PoseCfg(
position=(0.40, -0.20, 0.10),
quaternion_wxyz=(1.0, 0.0, 0.0, 0.0),
),
PoseCfg(
position=(0.40, 0.20, 0.10),
quaternion_wxyz=(1.0, 0.0, 0.0, 0.0),
),
)
)
move_cube = SegmentCfg(
name="move_cube",
steps=SequenceCfg(
items=(
InvokeCfg(
call=PickCfg(
object="cube",
resources={"primary": "manipulator"},
)
),
InvokeCfg(
call=PlaceCfg(
object="cube",
at=TargetRefCfg(target="drop_pose"),
resources={"primary": "manipulator"},
)
),
)
),
post=(WaitStablePostCfg(entity="cube"),),
validators=(
ObjectNearTargetValidatorCfg(
object="cube",
target="drop_pose",
position_tolerance=0.03,
),
),
)
return TaskProgramCfg(
program_id="python_pick_and_place",
integration=TaskProgramIntegrationCfg(
robot_profile="tutorial_robot",
scene_registry="tutorial_scene",
runtime_preset="trajectory",
),
targets={"drop_pose": drop_poses},
program=RepeatCfg(count=3, body=move_cube),
)
The three values in TaskProgramIntegrationCfg are exact identifiers
for the robot profile, scene registry, and runtime preset that a trusted
deployment will later provide. Provider-independent compilation preserves
those selections but does not construct or contact their live providers.
The program remains executable-free even though it is authored in Python. Do not put callbacks, simulator objects, planners, or controller commands in a Task Program. Registered extensions also accept only declarative arguments; their executable lowerers belong to the trusted integration.
Compile and inspect the program#
TaskProgramCompiler resolves the scene references, expands the bounded
repeat, selects each cyclic target, and assigns stable segment and call indices:
def print_compiled_program(compiled: CompiledTaskProgram) -> None:
"""Print the deterministic expansion produced by the compiler."""
print(f"Program: {compiled.program_id}")
print(f"Segments: {compiled.segment_count}")
for segment in compiled:
repeat = segment.repeat_frames[-1]
call_names = " -> ".join(type(item.call).__name__ for item in segment.calls)
target = segment.calls[-1].target_selections[0]
print(
f"[{segment.segment_index}] {segment.name} "
f"(repeat {repeat.iteration_index + 1}/{repeat.count}): "
f"{call_names}; {target.target_id}[{target.value_index}]"
)
def main() -> None:
"""Compile and inspect the tutorial Task Program."""
scene_manifest = build_scene_manifest()
program = build_program()
compiled = TaskProgramCompiler(scene_manifest).compile(program)
print_compiled_program(compiled)
if __name__ == "__main__":
main()
The returned CompiledTaskProgram is immutable and can be iterated more
than once with the same result. Segment post-policies and validators are also
resolved during compilation, but they run only after an environment adapter
binds the compiled program to live services.
Understand the boundary#
This tutorial stops at the correct provider-independent boundary. Compilation does not:
observe object poses or robot state;
select planners or generate controller commands;
verify that the named robot profile and runtime preset are installed; or
call
env.step().
Those responsibilities belong to the trusted integration, Atomic Skills, and the Gym execution bridge. Keeping them out of this example makes the Task Program language independently testable and keeps task intent reusable across compatible embodiments.
Diagnose an invalid reference#
Compiler failures include a stable error code and an exact source path. For example, compiling the same program against an empty scene manifest fails before any provider can be touched:
from embodichain.lab.task_program import (
TaskProgramCompileError,
TaskProgramCompiler,
render_config_path,
)
from embodichain.lab.task_program.semantics import SceneManifest
try:
TaskProgramCompiler(SceneManifest()).compile(build_program())
except TaskProgramCompileError as error:
print(error.code, render_config_path(error.path))
Use the reported path to fix the source declaration. Do not catch a compile error and continue into execution with a partially resolved program.
Next steps#
Creating a Modular Environment — learn how
EmbodiedEnvassembles a modular runtime.Configure and Run an Embodied Task Program — deploy and execute a Task Program through typed task, embodiment, scene, and execution-policy components.
Embodied Task Program — study the full language, semantic, and integration architecture.
Atomic actions — directly construct and execute typed Atomic Skills.