Overview
RPCs let one participant invoke a named method on another participant and await the result. The target participant registers a handler for the method, the caller invokes it with a request payload, and the handler runs and returns a response. The call resolves on the caller only once the handler completes, so it maps naturally to actions that need confirmation or a return value.
Delivery is reliable: the request and response are guaranteed to arrive, or the call fails with a timeout or an app error code the handler defines. Each call targets a single participant by identity, and the request and response payloads are strings, so serialize structured data as JSON or another text format. Because the caller waits for the handler to finish, use RPC for discrete operations rather than continuous, high-frequency data.
Use RPC to trigger robot behaviors or policy execution, request an on-demand sensor reading, manage devices, or query status and capabilities. For continuous streaming, use data tracks. For shared state that every participant should see, use participant attributes.
Example: Run control policy
The robot registers an RPC handler for the move_to method. The handler receives a string describing the target object, passes it to a model (for example, a vision-language-action model) that performs the action, and waits for the action to complete before returning a response or error.
async fn move_to(data: RpcInvocationData) -> Result<String, RpcError> {let description = data.payload;println!("'{}' invoked move_to with '{}'",data.caller_identity,description);my_model.move_to(description).await.map_err(|err| RpcError {code: RpcErrorCode::ApplicationError as u32,message: err.to_string(),data: None,})?;Ok(String::new())}room.local_participant().register_rpc_method("move_to".to_string(), |data|Box::pin(move_to(data)));
@room.local_participant.register_rpc_method("move_to")async def move_to(data: RpcInvocationData):description = data.payloadprint(f"'{data.caller_identity}' invoked move_to "f"with '{description}'")# An exception raised here reaches the caller as an# app error.await my_model.move_to(description)# No response payload is needed. Returning signals completion.return ""
if (auto lp = room->localParticipant().lock()) {lp->registerRpcMethod("move_to",[](const livekit::RpcInvocationData& data)-> std::optional<std::string> {std::cout << "'" << data.caller_identity<< "' invoked move_to with '"<< data.payload << "'\n";if (!my_model.moveTo(data.payload)) {return std::nullopt;}// No response payload is needed.return "";});} else {std::cerr << "Failed to get local participant\n";return;}
room.registerRpcMethod('move_to',async (data: RpcInvocationData) => {const description = data.payload;console.log(`'${data.callerIdentity}' invoked move_to with '${description}'`);try {await myModel.moveTo(description);} catch (error) {throw new RpcError(1, String(error));}// No response payload is needed. Returning signals completion.return '';});
room.localParticipant?.registerRpcMethod('move_to',async (data: RpcInvocationData) => {const description = data.payload;console.log(`'${data.callerIdentity}' invoked move_to with '${description}'`);try {await myModel.moveTo(description);} catch (error) {throw new RpcError(1, String(error));}// No response payload is needed. Returning signals completion.return '';});
A teleoperator can invoke the RPC method when a policy action is needed:
let data = PerformRpcData::new("robot", "move_to").with_payload("red bouncy ball");// Resolves only once the robot's handler returns, meaning the robot has// finished moving to the ball or failed trying.match room.local_participant().perform_rpc(data).await {Ok(_) => println!("Policy executed successfully"),Err(err) => println!("Failed to execute policy: {}", err)}
try:# Resolves only once the robot's handler returns, meaning the# robot has finished moving to the ball or failed trying.await room.local_participant.perform_rpc(destination_identity="robot",method="move_to",payload="red bouncy ball",)print("Policy executed successfully")except Exception as e:print(f"Failed to execute policy: {e}")
try {if (auto lp = room->localParticipant().lock()) {// Returns only once the robot's handler returns, meaning the// robot has finished moving to the ball or failed trying.lp->performRpc("robot", "move_to", "red bouncy ball");std::cout << "Policy executed successfully\n";}} catch (const livekit::RpcError& error) {std::cerr << "Failed to execute policy: " << error.what() << "\n";}
try {// Resolves only once the robot's handler returns, meaning the// robot has finished moving to the ball or failed trying.await room.localParticipant.performRpc({destinationIdentity: 'robot',method: 'move_to',payload: 'red bouncy ball',});console.log('Policy executed successfully');} catch (error) {console.error('Failed to execute policy:', error);}
try {// Resolves only once the robot's handler returns, meaning the// robot has finished moving to the ball or failed trying.await room.localParticipant?.performRpc({destinationIdentity: 'robot',method: 'move_to',payload: 'red bouncy ball',});console.log('Policy executed successfully');} catch (error) {console.error('Failed to execute policy:', error);}
Additional resources
Remote procedure calls
Full RPC API and examples in every client SDK.