step message and the instruction behind each chunk. The instruction starts as “Put the marker in the cup.” and changes to “Open the drawer.” after six chunks.
Key Facts
What the App Does
The client sends a text instruction, an observation image, and the robot joint state. For eachstep message, the app returns two outputs:
- An action chunk on the control channel (the JSON message channel that runs next to the video). It has 32 steps, and each step has 7 joint positions and 1 gripper value (DROID, D=8).
- The predicted video for that chunk on the outgoing WebRTC track, at 15 frames per second.
step to action_chunk. The first call after start took about 3 seconds.
Each chunk returns 33 predicted frames at 640x528. The first frame appears to repeat the observation. The app plays all 33 frames of the first chunk. For later chunks it skips the first frame, so 32 frames play for about 2.1 seconds.
The app shows the ConfiguredSession interaction profile from fal.wma.profile:
- The client sends
configurewith the first instruction and the initial state. - The app sends
configured. - The client sends
stepmessages. It can also sendinstructionmessages to change the task.
actions has 7 joint positions in radians and 1 gripper value from 0 to 1. The arm is a DROID (Franka-style) arm.
The rows use the same joint space as the joint_position input. The control rate is 15 Hz.
One chunk has 32 rows, so one chunk covers about 2.1 seconds of open-loop motion.
Prerequisites
- A fal account with permission to deploy and call the app. The browser client connects through fal’s WMA service at
wma.fal.run. It needs a fal API key on your server, or a signed-in dashboard session for the playground. - Access to the fal WMA TURN servers for the account that owns the app (the
Xinfal.run/X/app). Request it from fal before you rely on the app. Without it, a session connects only when the runner is directly reachable, which fails on many runners and networks. See If the Session Does Not Start. - Python 3.13 on your computer. The app image uses Python 3.13, and the Python versions must match for deployment.
- The fal CLI and the demo code. Run these commands:
- A DROID-style observation image at an
httporhttpsURL. Put the wrist camera view on top, and the two exterior camera views side by side below it. The model trained on this layout.
GPU and Runtime
The app uses oneGPU-H100 runner. The checkpoint is about 9 GB, and the model peaked at about 11.6 GB of GPU memory in our tests.
NVIDIA does not publish a minimum VRAM figure. The model card lists H100 as a tested platform.
The app does not need vLLM-Omni. It uses the PyTorch service from the Cosmos framework.
The Cosmos framework needs PyTorch 2.13 with CUDA 13 and a Python 3.13 environment. The app builds this environment in a custom container.
The first image build is slow. The first start downloads the weights to /data/hf.
The Dockerfile starts from a CUDA 13 base image and installs uv, the Cosmos framework, and fal[wma]:
The App
View the complete source code on GitHub. This page shows the parts that use the WMA SDK.The App Class
The class builds a custom container from a Dockerfile string, because the Cosmos framework needs its own PyTorch and CUDA version. It also sets the runner and a longstartup_timeout for the weight download.
min_concurrency = 0 value scales the app to zero, so the first session waits for a cold start. The keep_alive value is the idle seconds that a runner stays up after its last session, and fal bills this time. The max_concurrency and max_multiplexing values are the most runners and the most sessions on one runner. They are example limits. Adjust them for your workload and cost. See Scale your application.
The Contract
The contract declares one video track, the typed messages, and theConfiguredSession profile.
The Message Handlers
Theinstruction handler keeps the newest text and sends instruction_pending or instruction_rejected.
The Session Backend
Thecreate_backend method registers one handler for each message and ends the session after SESSION_SECONDS.
The lines that build the ICE servers (the RunnerIceConfig call) are boilerplate. Copy them as they are.
How the Contract Maps to Client Controls
Send
step_id as a positive integer. The app copies it into the action_chunk message.
The instruction_version values must increase. The first value is in configure.
A step can carry joint_position, gripper_position, and image_url.
When the client sends them, the model starts from the real robot state and camera view.
When the client omits them, the app continues from the last predicted action and the last predicted frame.
This second mode is an imagined rollout. Prediction errors add up over many chunks.
Send one step at a time. Send the next step only after the action_chunk for the previous step arrives.
A step that arrives while a chunk generates returns an error with code busy.
A chunk takes about 0.9 seconds to generate. It covers about 2.1 seconds of robot time, so a loop with no wait runs ahead of playback.
The video queue holds four chunks and drops the oldest frames when it is full.
For video that stays near real time, send the next step when the robot finishes the chunk.
The video and the actions are not time-aligned on the wire. Use step_id to match an action chunk to its step. Treat the video as a preview.
Fatal errors have the codes invalid_configuration, invalid_image, and generation_failed. The app closes the session after them.
An invalid image_url in configure or in step gives invalid_image.
Run and Deploy
From the root of the cloned repository, usefal run as the development loop. It starts the app on an ephemeral runner:
Cost and First Start
You pay per second for the runner, including setup time and the idlekeep_alive time. See Pricing. The billable units in the app matter only if you list the app on the fal Marketplace. Callers then pay for these units at a price that the fal team sets.
The fal run and fal deploy commands build the container image. In our tests, the build took about 3 minutes.
The first session starts a runner and downloads the checkpoint of about 9 GB.
Watch the runner logs in the dashboard. The /data volume keeps the weights across runners. See persistent storage.
To keep a runner warm, set min_concurrency = 1. A warm runner costs idle time. See cold starts.
Clean Up
To stop paying for the app, scale it to zero withfal apps scale <app-name> --min-concurrency 0. To remove it, run fal apps delete <app-name>.
Try It
Open the deployed app’s/start-session page in the fal playground and select Connect.
The playground reads the app contract and offers the example messages.
- Send the Configure example. The example
image_urlis a sample DROID observation. It combines frame 0 of the three camera views from thedroid_lerobot_exampleassets in the pinned cosmos-framework commit. The fal CDN hosts it. Replace it with a public URL of your own DROID-style image. - Wait for the
configuredmessage. - Send Step from predicted state. The app answers with an
action_chunkmessage and sends the predicted video. - Send New instruction, then send another step.
Browser Client
There is no supported Python or Node client. Contact fal if you need one. The supported client is the experimental WMA browser client. It runs in a browser.- Set up the server proxy as the browser client page describes.
- Install the client and the proxy package in your web application.
- Add the code below. The API key stays on your server behind the proxy route.
your-account/<app-name>. The name comes from the class name, so RobotWorldModel becomes robot-world-model.
Use receive: ["video"] and no localStream. The onData callback receives the raw JSON string. Parse it before you use it.
The code waits for configured before the first step, then sends each next step when the robot finishes.
If executeActions fails, the code closes the session. The ended flag makes the cleanup run once.
step.
A session can end on its own. The app closes each session after 15 minutes, and it closes the session after a fatal error.
Server Messages
The
actions array is truncated in the example. It has 32 rows of 8 values.
If the Session Does Not Start
Billing
This section applies if you list the app on the fal Marketplace. For your own private app, you pay only for runner time, and the units do not change the cost. The app callssession.add_billable_units(1) after each completed chunk. One unit is one action chunk.
The fal team configures the price per unit when it lists the app. See billing in the WMA reference.
Runner compute can also incur charges. Disconnect when you finish.
Limits and Caveats
- The app serves one session at a time on each runner (
max_multiplexing = 1). - The model works only for the DROID robot (8 values). It expects the three-view image layout.
- Add your own validation and safety limits before a real robot runs the actions.
- A model call that already started cannot stop. If the session ends, the app discards the result and does not bill it. The next session’s model call waits until that call finishes.