# Phase 6: Inline module

Optional: package your working script as an inline module that runs on the robot.
> Source: https://docs.viam.com/tutorials/pick-and-place/inline-module/


This phase is optional. The previous phase already gave you a complete pick-and-place loop that runs from your own laptop: detection, planned motion, and a reliable place. This phase packages that same loop as a module so it runs on the robot directly, with no laptop connection required once it is deployed.

## Why use a module

A [module](/build-modules/) packages a resource so `viam-server` can run it directly, the same way the `ufactory` and `realsense` modules you added earlier run their drivers. Modules are the unit Viam shares and deploys: they live in a registry where they can be reused across machines, they carry versions so you can roll a deployment forward or back, and they build for each platform your fleet runs. Packaging your pick-and-place loop as a module puts your pick-and-place code on that same footing as any driver.

For your own robot logic, reach for a module when one of these is true for your setup:

- The cycle has to keep running after you close your laptop or walk away.
- The cycle has to restart on its own if it crashes or the robot reboots.
- You want to deploy an updated version to the robot without pushing code from a laptop by hand.
- You want the cycle to run on a schedule instead of a manual trigger.

If none of those apply, stop here. You have already built the thing this workshop set out to teach.

This phase builds an **inline module**: the Viam app hosts the code and builds it for you in the cloud, with the source in an editor in your browser. A conventional module lives in its own Git repository that you build and upload yourself, and it is the better choice for code you share across many machines or maintain as a team. Inline is the faster path for a single machine's own logic, so it is what you use here; everything you learn about the module lifecycle applies to both.

## What changes when you package it

The detection, the pose math, and the motion calls are the same pick-and-place logic, moved into a module's lifecycle methods with no change to what they do. Only the wiring around that logic changes, in a few places:

- **No connection code necessary.** The module is part of the machine, so authentication is no longer necessary.
- **How the module gets its resources.** A script calls `from_robot` after it connects; a module receives its resources through **dependency injection** instead.
- **Config-driven resources.** Your local script uses hardcoded component names like "arm-1." The module reads those names dynamically, which allows a module to be used across different machines that may have different naming conventions.
- **How the logic gets triggered.** A script runs top to bottom under `__main__`; a module runs its logic behind a **`do_command`** entry point that something else calls.
- **Hard constraints in class structure.** For an inline module, the Viam app auto-generates the model identifier and class name, and you must not change them.

## Create the module

On the **Configure** tab, click the **+** icon then **Code**. Choose a "Viam-hosted" module, click **Proceed**, and select **Python** as the language. The Viam app creates a new configured resource with an embedded code editor and a generated module skeleton.

<!-- ASSET P0 inline-module-editor (UI+): the inline module editor open in Configure with code pasted. See plans/2026-07-02-pick-and-place-shot-list.md -->






    
    
    
<picture>

  
  
<source srcset="/tutorials/pick-and-place/inline-module-editor_hu_3cb34fee2e3453f5.webp" type="image/webp" width="1200" height="709">
<img src="/tutorials/pick-and-place/inline-module-editor.png" width="1200" height="709" alt="The inline module code editor open in Configure with the generated Python skeleton." class="" id="" style="" loading="lazy">
  

</picture>




Logic like this is typically modeled as a **generic service**: a resource that exposes no specialized API of its own, so you can put arbitrary logic behind it. The skeleton builds that service on the `EasyResource` mixin, a convenient base class from the Viam SDK that fills in the boilerplate every resource needs and lets you override only the parts you care about. Those parts are **lifecycle methods**: functions the module framework calls at set points, such as `validate_config` when the config is checked, `new` when the resource starts, and `close` when it shuts down. One more method, **`do_command`**, is the generic service's entry point: because the service has no typed API, `do_command` is how a caller runs the actions it exposes.

> **Module identification on the registry:**
> 
> 
> One value in that skeleton you should not change is the class's **`MODEL`**: a three-part `namespace:module-name:model-name` identifier, such as `your-org:your-module:pick-and-place`, that the Viam app generates when it creates the module. Together with the class name, it names your model uniquely on the registry, and the platform matches your code to its registry registration by exactly these values. Leave the `MODEL` and the class name as generated; changing either breaks that match, and the module fails to build.
> 

Over the next two sections you move your pick-and-place code into this skeleton: the typed resource handles become dependency injection through `new`, and the detection, pose math, and motion calls gather behind the `do_command` entry point. Once all the code is in place, you can save and build your code block, then run it on the robot.

## Dependency injection

The same resources reach your code differently in a script versus a module:

<!-- ASSET P1 diagram-script-vs-module (DIAGRAM): from_robot(...) vs new() attribute-named deps dependencies[Arm.get_resource_name(attrs["arm"])], resource names set once as config attributes -->

```text
Script  (Phases 4 and 5)
  connect  →  RobotClient.at_address(...)
  arm      =  Arm.from_robot(machine, "arm-1")

Module  (Phase 6)   (same logic, different wiring)
  validate_config()   declares dependencies from the config attributes
  new(config, deps)   looks up each dependency by its configured name
```

A script builds its resource handles once, right after it connects, by calling `Arm.from_robot(machine, "arm-1")` and similar for each resource it needs. A module does not connect to itself, so it cannot call `from_robot` the same way. Instead, the module framework hands your module its dependencies.

Two lifecycle methods carry this pattern:

- `validate_config` runs before your module starts and declares which resources it depends on, so `viam-server` knows to hold your module back until those resources are online, the same dependency ordering you already saw between `gripper-1` and `arm-1` in Phase 2. It reads the module's own config attributes, where each attribute value is the name of a resource on the machine, and returns those names as the required dependencies.
- `new` receives the resolved dependencies as a mapping keyed by resource name, and this is where you build the typed handles your pick-and-place logic calls. `new` is the `EasyResource` classmethod that constructs your resource; it reads the same config attributes to look each dependency up by its configured name.

A small illustrative sketch of both methods:

```python
from viam.components.arm import Arm
from viam.utils import struct_to_dict


@classmethod
def validate_config(cls, config):
    attrs = struct_to_dict(config.attributes)
    required_deps = []
    if "arm" not in attrs or not attrs["arm"]:
        raise ValueError("attribute 'arm' (non-empty string) is required")
    required_deps.append(attrs["arm"])
    # ...same for gripper, camera, "home-pose", "travel-pose", "place-pose", vision...
    required_deps.append("builtin")  # motion service
    return required_deps, []


@classmethod
def new(cls, config, dependencies):
    self = super().new(config, dependencies)
    attrs = struct_to_dict(config.attributes)
    self.arm = dependencies[Arm.get_resource_name(attrs["arm"])]
    # ...look up gripper, camera, the pose switches, and vision the same way...
    return self
```

Keep the rest of your `new` close to this shape: look up each resource your existing Python script used by its configured attribute name, and store it on `self` so your pick-and-place logic can call it later. Note that the derived poses for approach and grasp do not need to be declared in new or validate config.

The motion service is injected the same way. `validate_config` declares it as `"builtin"`, and `new` retrieves it with `dependencies[MotionClient.get_resource_name("builtin")]` (from `viam.services.motion import MotionClient`), so `motion.move` works straight from the injected dependencies.

> **Same resource names, different retrieval:**
> 
> 
> Compare how you got the arm handle in the Python script against how you get it inside the module:
> 
> - Local script (Phases 4-5): `arm = Arm.from_robot(machine, "arm-1")`.
> - Module: `arm = dependencies[Arm.get_resource_name(attrs["arm"])]`.
> 
> The resource name is still `"arm-1"` in both. In the module you set it once as the module's `arm` config attribute, which the operator points at `arm-1`, and `attrs["arm"]` reads it back. The same is true of the gripper, camera, every pose switch, the vision service, and the motion service. Only the retrieval mechanism changes, from calling `from_robot` on a connected `machine` handle to looking the resource up in the `dependencies` mapping `new` received.
> 

## Trigger the module with do_command

With dependencies wired up, assemble your pick-and-place logic into a single `run_pick_cycle` method on the module, the same detection, pose math, and motion calls, unchanged. What differs is how that method gets triggered.

Assembled onto the module, using the dependencies you looked up in `new`, `run_pick_cycle` looks like this:

```python
async def run_pick_cycle(self) -> bool:
    """Run one detect-pick-place cycle. Returns False if no object was detected, True on a completed cycle."""
    # 1. Observe from home so the wrist-mounted camera frame is in a known position.
    await self.home_pose.set_position(2)
    # 2. Detect. vision-segment fuses the 2D shape detections with depth into
    #    3D objects, each with a point cloud and a label.
    objects = await self.vision.get_object_point_clouds(self.camera_name)
    if not objects:
        print("No objects detected")
        return False
    # Largest object by point-cloud byte size (a proxy for point count).
    # point_cloud is raw PCD bytes, so use len(point_cloud), not .size.
    obj = max(objects, key=lambda o: len(o.point_cloud))
    geometry = obj.geometries.geometries[0]
    label = geometry.label
    print(f"Detected: {label}")
    # 3. Derive the approach pose from the object center, in the camera
    #    frame. cam-1 is wrist-mounted, but the approach move is resolved
    #    from the camera frame while the arm is still at home, so it lands
    #    accurately above the block.
    obj_in_cam = PoseInFrame(reference_frame=self.camera_name, pose=geometry.center)
    approach_pose = offset_pose(obj_in_cam.pose, APPROACH_MM)
    # 4. Pick: move above in the camera frame, open, then descend the
    #    remaining distance straight down in the gripper's own frame, grab,
    #    lift. Descending relative to the gripper avoids the wrist-mounted
    #    camera frame shifting once the arm moves for the approach.
    await self.motion.move(
        self.gripper_name,
        PoseInFrame(reference_frame=self.camera_name, pose=approach_pose),
    )
    await self.gripper.open()
    await asyncio.sleep(SETTLE_S)
    # Move the remaining distance from the approach pose (as a positive value).
    grasp_distance = (APPROACH_MM - GRIPPER_LENGTH_MM) * -1
    await self.motion.move(
        self.gripper_name,
        PoseInFrame(
            reference_frame=self.gripper_name,
            pose=Pose(x=0, y=0, z=grasp_distance, o_x=0, o_y=0, o_z=1, theta=0),
        ),
    )
    await self.gripper.grab()
    await asyncio.sleep(SETTLE_S)
    # 5. Place: lift to the safe carrying height, drop at the saved bin pose.
    await self.travel_pose.set_position(2)
    await self.place_pose.set_position(2)
    await self.gripper.open()
    await self.home_pose.set_position(2)

    return True
```

You trigger the module through `do_command`. Because a generic service has no typed API of its own, `do_command` is its entry point: the method you dispatch on to run the actions your service exposes. A small illustrative sketch:

```python
async def do_command(self, command, *, timeout=None, **kwargs):
    if command.get("action") == "pick_cycle":
        success = await self.run_pick_cycle()
        return {"success": success}
    return {}
```

> **Module build feedback loop:**
> 
> 
> Saving an inline Python module triggers a cloud build that takes about a minute. Give it that minute rather than assuming the save failed.
> 

With the code in place, save the module. The Viam app packages it and deploys it to the machine, and the **Logs** tab shows the build progress the same way it showed module downloads back in Phase 2.

<!-- ASSET P1 logs-cloud-build (UI): Logs showing the ~1 min cloud build + module start -->






    
    
    
<picture>

  
  
<source srcset="/tutorials/pick-and-place/logs-cloud-build_hu_d76a5a14deace05e.webp" type="image/webp" width="1200" height="608">
<img src="/tutorials/pick-and-place/logs-cloud-build.png" width="1200" height="608" alt="The Logs tab showing the inline module cloud build." class="" id="" style="" loading="lazy">
  

</picture>




<div class="checkpoint">
  <p class="checkpoint-title"><i class="icon fas fa-check-circle"></i>Checkpoint</p>
  <div class="checkpoint-body">The module finishes its cloud build and starts without errors in the <strong>Logs</strong> tab, and its resource shows online on the <strong>Configure</strong> tab. If the build fails, read the build log for the specific error; a missing import or a syntax error carried over from the script is the most common cause.</div>
</div>


From the **Control** tab, find your module's test card and send a command such as `{"action": "pick_cycle"}` to run one full pick-and-place cycle on demand, the same cycle you watched run from your script, now running on the robot instead of your personal computer. To compare your module against a finished one, read the complete [`module-reference.py`](https://github.com/viam-devrel/pick-and-place/blob/main/scripts/module-reference.py) in the companion repo.

<!-- ASSET P1 control-do-command (UI+): triggering the do_command from the app -->






    
    
    
<picture>

  
  
<source srcset="/tutorials/pick-and-place/control-do-command_hu_a7839dd21d9c6106.webp" type="image/webp" width="1200" height="499">
<img src="/tutorials/pick-and-place/control-do-command.png" width="1200" height="499" alt="The module DoCommand test card running the pick_cycle action." class="" id="" style="" loading="lazy">
  

</picture>




<div class="checkpoint">
  <p class="checkpoint-title"><i class="icon fas fa-check-circle"></i>Checkpoint</p>
  <div class="checkpoint-body">Sending a <code>do_command</code> trigger runs one complete pick-and-place cycle: detection, approach, grasp, travel, and place, ending with a block in the bin.</div>
</div>


## Where you landed

You now have the same pick-and-place loop running two ways: as a script you control from your personal computer, and as a module that keeps running on the robot. Head to the [wrap-up](/tutorials/pick-and-place/wrap-up/) to review everything you built and where to take it next.

<nav class="workshop-nav" aria-label="Workshop phase navigation"><p class="workshop-progress">Phase 6 of 6</p><div class="workshop-nav-links"><a class="workshop-nav-prev" href="/tutorials/pick-and-place/perception-guided-picking/">&larr; Previous</a><a class="workshop-nav-next" href="/tutorials/pick-and-place/wrap-up/">Next &rarr;</a></div>
</nav>


