Twin Messaging
This guide covers hub-based messaging between twins using Events and Actions. For real-time streaming and low-latency communication, see Real-Time Channels.
What Is Twin Messaging?
Section titled “What Is Twin Messaging?”Twin messaging lets your apps communicate, whether they’re running on the same device or across different locations. PhyStack intelligently routes messages locally within a device when possible, or through the cloud hub when apps are on different devices. Your code stays the same either way.
Every message has:
- A type (string) - identifies what kind of message this is
- A payload (object) - the data being sent
There are two patterns:
| Pattern | Description | Use Case |
|---|---|---|
| Events | Fire-and-forget messages | Status updates, notifications, telemetry |
| Actions | Request-response with callback | Commands that need confirmation, device control |
Getting Started
Section titled “Getting Started”Twin messaging uses the @phystack/hub-client (TypeScript) or phystack-hub-client (Python) package. Your app connects to PhyHub automatically, either through PhyOS on a real device, or through the local simulator during development.
import { connectPhyClient } from '@phystack/hub-client';
const client = await connectPhyClient();const instance = await client.getInstance();
// Now you can send and receive messagesinstance.on('myEvent', (data) => { console.log('Received:', data);});from phystack.hub_client import connect_phy_client
client = await connect_phy_client()instance = await client.get_instance()
# Now you can send and receive messagesdef on_my_event(data, respond=None): print('Received:', data)
instance.on('myEvent', on_my_event)Twin IDs Explained
Section titled “Twin IDs Explained”To send a targeted message, you need the recipient’s twin ID (sometimes also called instance ID). Where this comes from depends on the context. You’ll see twin IDs used throughout the examples later in this guide.
- Simulator (local dev): Start the simulator with
phy simulator start, then run your app withyarn dev. The simulator assigns each app a twin ID visible in the logs:Copy twin IDs from the simulator output and use them to populate your app’s local settings values.Instance connected: my-org.my-app (fb1f8cc4-73d0-490a-82dc-7a917cbed2e2) — 1 active - Production: Twin IDs come from app settings configured in the PhyStack console. Use instance picker fields in your settings schema to let users select which twin to communicate with.
In both cases, your app typically reads twin IDs from settings at runtime:
const { targetTwinId } = settings;instance.to(targetTwinId).emit('hello', { message: 'Hi!' });Events vs Actions
Section titled “Events vs Actions”Events (Fire-and-Forget)
Section titled “Events (Fire-and-Forget)”Events are simple messages that don’t expect a response. Use events when you just need to notify other twins about something.
// Broadcast to all subscribersinstance.emit('statusUpdate', { status: 'ready', temperature: 25 });
// Send to a specific twininstance.to(targetTwinId).emit('statusUpdate', { status: 'ready' });
// Receiving an eventinstance.on('statusUpdate', (data) => { console.log('Status:', data.status);});# Broadcast to all subscribersinstance.emit('statusUpdate', {'status': 'ready', 'temperature': 25})
# Send to a specific twininstance.to(target_twin_id).emit('statusUpdate', {'status': 'ready'})
# Receiving an eventdef on_status_update(data, respond=None): print('Status:', data['status'])
instance.on('statusUpdate', on_status_update)Actions (Request-Response)
Section titled “Actions (Request-Response)”Actions are messages that expect a response. Use actions when you need confirmation that something completed successfully. Actions require targeting a specific twin using to().
// Sending an action — to().emit() with a callback triggers request-responseinstance.to(targetTwinId).emit('runDiagnostics', { level: 'full' }, (result) => { console.log('Diagnostics result:', result.status, result.message);});
// Handling an action and respondinginstance.on('runDiagnostics', (data, respond) => { const passed = runTests(data.level); respond({ status: passed ? 'success' : 'error', message: passed ? 'All tests passed' : 'Tests failed', });});from phystack.hub_client import TwinMessageResult, TwinMessageResultStatus
# Sending an action — to().emit() with a callback triggers request-responsedef on_result(result): print('Diagnostics result:', result.status, result.message)
instance.to(target_twin_id).emit('runDiagnostics', {'level': 'full'}, on_result)
# Handling an action and respondingdef on_run_diagnostics(data, respond=None): passed = run_tests(data['level']) if respond: respond(TwinMessageResult( status=TwinMessageResultStatus.SUCCESS if passed else TwinMessageResultStatus.ERROR, message='All tests passed' if passed else 'Tests failed', ))
instance.on('runDiagnostics', on_run_diagnostics)The response follows the TwinMessageResult structure:
interface TwinMessageResult { status: 'success' | 'error' | 'warning'; message?: string;}# TwinMessageResult dataclass with TwinMessageResultStatus enumfrom phystack.hub_client import TwinMessageResult, TwinMessageResultStatus
result = TwinMessageResult( status=TwinMessageResultStatus.SUCCESS, # or ERROR, WARNING message='Operation completed',)Sending Events
Section titled “Sending Events”Broadcast Events
Section titled “Broadcast Events”Send events to all subscribers of your twin:
instance.emit('statusUpdate', { status: 'ready' });instance.emit('sensorData', { temperature: 25.5, humidity: 60 });instance.emit('statusUpdate', {'status': 'ready'})instance.emit('sensorData', {'temperature': 25.5, 'humidity': 60})Targeted Events
Section titled “Targeted Events”Send events to a specific twin:
instance.to(targetTwinId).emit('displayMessage', { title: 'System Alert', content: 'Maintenance scheduled for 10:00 PM',});instance.to(target_twin_id).emit('displayMessage', { 'title': 'System Alert', 'content': 'Maintenance scheduled for 10:00 PM',})Receiving Events
Section titled “Receiving Events”Register handlers to receive events:
instance.on('command', (data) => { console.log('Received command:', data);});
instance.on('sensorReading', handleSensorData);instance.on('configUpdate', handleConfigChange);def on_command(data, respond=None): print('Received command:', data)
instance.on('command', on_command)instance.on('sensorReading', handle_sensor_data)instance.on('configUpdate', handle_config_change)Sending Actions
Section titled “Sending Actions”Actions wait for a response from the handler. Pass a callback as the third argument to to().emit():
instance.to(targetTwinId).emit('calibrateSensor', { sensorId: 'temp-001', targetValue: 25.0,}, (result) => { console.log('Calibration result:', result.status, result.message);});def on_calibrate_result(result): print('Calibration result:', result.status, result.message)
instance.to(target_twin_id).emit('calibrateSensor', { 'sensorId': 'temp-001', 'targetValue': 25.0,}, on_calibrate_result)Actions have a 10-second timeout. If no response is received, the callback receives a timeout error.
Note: Actions require a target twin. Use
instance.to(targetId).emit()for request-response patterns. The broadcastinstance.emit()is fire-and-forget only.
Handling Actions
Section titled “Handling Actions”When handling actions, you receive a respond function to send back results:
instance.on('restartService', (data, respond) => { const success = serviceManager.restart(data.serviceName); respond({ status: success ? 'success' : 'error', message: success ? `Service ${data.serviceName} restarted` : 'Restart failed', });});def on_restart_service(data, respond=None): success = service_manager.restart(data['serviceName']) if respond: respond(TwinMessageResult( status=TwinMessageResultStatus.SUCCESS if success else TwinMessageResultStatus.ERROR, message=f"Service {data['serviceName']} restarted" if success else 'Restart failed', ))
instance.on('restartService', on_restart_service)Common Patterns
Section titled “Common Patterns”Edge to Screen Communication
Section titled “Edge to Screen Communication”Edge app sends data to screen for display. For example, a barcode scanner edge app reads a product barcode and pushes the product details to a digital signage screen for the customer to see.
// Edge app — scan barcode and send product to screenconst product = await lookupProduct(barcode);instance.to(screenTwinId).emit('showProduct', { name: product.name, price: product.price, image: product.imageUrl,});
// Screen app — display product to customerinstance.on('showProduct', (data) => { displayProductCard(data.name, data.price, data.image);});# Edge app — scan barcode and send product to screenproduct = await lookup_product(barcode)instance.to(screen_twin_id).emit('showProduct', { 'name': product['name'], 'price': product['price'], 'image': product['imageUrl'],})Screen to Edge with Response
Section titled “Screen to Edge with Response”Screen requests an action from edge, edge confirms. For example, a self-checkout kiosk screen sends a print command to the edge app controlling a thermal receipt printer, and waits for confirmation before showing “Receipt printed” to the customer.
// Screen app — request actioninstance.to(edgeTwinId).emit('print', { text: 'Receipt #12345' }, (result) => { console.log('Print result:', result.status, result.message);});
// Edge app — handle and respondinstance.on('print', (data, respond) => { printer.print(data.text); respond({ status: 'success', message: 'Printed successfully' });});# Screen app — request actioninstance.to(edge_twin_id).emit('print', {'text': 'Receipt #12345'}, lambda result: print('Print result:', result.status, result.message))
# Edge app — handle and responddef on_print(data, respond=None): printer.print(data['text']) if respond: respond(TwinMessageResult( status=TwinMessageResultStatus.SUCCESS, message='Printed successfully', ))
instance.on('print', on_print)Peripheral Communication
Section titled “Peripheral Communication”Edge apps create peripheral twins to represent connected hardware (printers, sensors, scanners). Other apps discover and communicate with peripherals. For example, an edge app managing a temperature sensor creates a peripheral twin and broadcasts readings, a screen app subscribes and displays live data on a dashboard. For a full guide, see Peripherals and Descriptors.
// Edge app — create peripheral and listen for commandsconst peripheral = await instance.createPeripheralTwin( 'temperature-sensor', 'sensor-001', { model: 'TMP36', location: 'warehouse' },);
const sensorInstance = await client.getPeripheralInstance(peripheral.id);sensorInstance.on('read', (data, respond) => { respond({ status: 'success', message: 'Temperature: 25.5C' });});
// Periodically emit readings from the peripheralsetInterval(() => { sensorInstance.emit('sensorData', { temperature: 25.5, humidity: 60 });}, 5000);// Screen app — subscribe to peripheral dataconst sensorInst = await client.getPeripheralInstance(settings.peripheralTwinId);sensorInst.on('sensorData', (data) => { console.log('Temperature:', data.temperature);});# Edge app — create peripheral and listen for commandsperipheral = await instance.create_peripheral_twin( 'temperature-sensor', 'sensor-001', {'model': 'TMP36', 'location': 'warehouse'},)
sensor_instance = await client.get_peripheral_instance(peripheral['id'])
def on_read(data, respond=None): if respond: respond(TwinMessageResult( status=TwinMessageResultStatus.SUCCESS, message='Temperature: 25.5C', ))
sensor_instance.on('read', on_read)
# Periodically emit readings from the peripheralasync def sensor_loop(): while True: sensor_instance.emit('sensorData', {'temperature': 25.5, 'humidity': 60}) await asyncio.sleep(5)Checkpoint
Section titled “Checkpoint”You should now understand:
- The difference between Events and Actions
- When to use
emit()vsto().emit() - How to send and receive messages between twins
- Where twin IDs come from (simulator vs production)
- Common communication patterns (edge-screen, peripherals)
Next Steps
Section titled “Next Steps”- Communication Overview - Compare messaging approaches
- Real-Time Channels - WebRTC DataChannels and MediaStreams
- Peripherals and Descriptors - Hardware integration
- Settings Schemas - Configure twin IDs and app settings