Build PhyStack
Twin Messaging · Build PhyStack

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?

Twin messaging routes messages through the PhyStack hub. Every message has:

  • A type (string) - identifies what kind of message this is
  • A payload (object) - the data being sent

There are two patterns:

PatternDescriptionUse Case
EventsFire-and-forget messagesStatus updates, notifications, telemetry
ActionsRequest-response with callbackCommands that need confirmation, device control

Events vs Actions

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 subscribers
instance.emit('statusUpdate', { status: 'ready', temperature: 25 });

// Send to a specific twin
instance.to(targetTwinId).emit('statusUpdate', { status: 'ready' });

// Receiving an event
instance.on('statusUpdate', (data) => {
  console.log('Status:', data.status);
});

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 callback returns a Promise
try {
  const result = await instance.to(targetTwinId).emit('runDiagnostics', { level: 'full' }, () => {});
  console.log('Diagnostics completed:', result.message);
} catch (error) {
  console.error('Diagnostics failed:', error.message);
}

// Handling an action and responding
instance.on('runDiagnostics', async (data, respond) => {
  try {
    const diagnosticResult = await device.runDiagnostics(data.level);
    respond({ status: 'success', message: `All ${diagnosticResult.testsRun} tests passed` });
  } catch (error) {
    respond({ status: 'error', message: error.message });
  }
});

The response object follows this structure:

interface TwinMessageResult {
  status: 'success' | 'error' | 'warning';
  message?: string;
}

Getting Started

Prerequisites

  • PhyStack device or hub-client connected
  • Understanding of twin IDs and instances

Basic Setup

import { PhyHubClient } from '@phystack/hub-client';

const client = await PhyHubClient.connect();

// Get your instance
const instance = await client.getInstance();

Sending Events

Broadcast Events

Send events to all subscribers of your twin:

const instance = await client.getInstance();

// Send to all subscribers (fire-and-forget)
instance.emit('statusUpdate', { status: 'ready' });
instance.emit('sensorData', { temperature: 25.5, humidity: 60 });

Targeted Events

Send events to a specific twin:

const targetTwinId = 'screen-abc123';

// Send to specific twin (fire-and-forget)
instance.to(targetTwinId).emit('displayMessage', {
  title: 'System Alert',
  content: 'Maintenance scheduled for 10:00 PM'
});

Receiving Events

Register handlers to receive events:

const instance = await client.getInstance();

// Handle incoming events
instance.on('command', (data) => {
  console.log('Received command:', data);
});

// Handle multiple event types
instance.on('sensorReading', handleSensorData);
instance.on('configUpdate', handleConfigChange);
instance.on('alertTriggered', handleAlert);

Sending Actions

Actions wait for a response from the handler. Use to(targetId).emit() with a callback to get a Promise back:

const instance = await client.getInstance();
const targetTwinId = 'edge-abc123';

// to().emit() with callback returns a Promise
try {
  const result = await instance.to(targetTwinId).emit('calibrateSensor', {
    sensorId: 'temp-001',
    targetValue: 25.0
  }, () => {});
  console.log('Calibration complete:', result.message);
} catch (error) {
  console.error('Calibration failed:', error.message);
}

Actions have a 10-second timeout. If no response is received, the Promise rejects with a timeout error.

Note: Actions require a target twin. Use instance.to(targetId).emit() for request-response patterns. The broadcast instance.emit() is fire-and-forget only.

Handling Actions

When handling actions, you receive a respond function to send back results:

instance.on('restartService', async (data, respond) => {
  try {
    await serviceManager.restart(data.serviceName);
    respond({
      status: 'success',
      message: `Service ${data.serviceName} restarted`
    });
  } catch (error) {
    respond({
      status: 'error',
      message: error.message
    });
  }
});

Common Patterns

Edge to Screen Communication

Edge app sends commands to screen, screen responds:

// Edge app - fire-and-forget event
const screenTwinId = 'screen-abc123';
instance.to(screenTwinId).emit('updateDisplay', {
  mode: 'dashboard',
  data: { temperature: 25.5, humidity: 60 }
});

// Screen app
instance.on('updateDisplay', (data) => {
  renderDashboard(data.mode, data.data);
});

Screen to Edge with Response

Screen requests action from edge, edge confirms:

// Screen app - action with response
const edgeTwinId = 'edge-abc123';
try {
  const result = await instance.to(edgeTwinId).emit('executeCommand', {
    command: 'capture-snapshot',
    params: { resolution: '1080p' }
  }, () => {});
  if (result.status === 'success') {
    showConfirmation('Snapshot captured');
  }
} catch (error) {
  console.error('Command failed:', error.message);
}

// Edge app
instance.on('executeCommand', async (data, respond) => {
  const success = await commandExecutor.run(data.command, data.params);
  respond({
    status: success ? 'success' : 'error',
    message: success ? 'Command executed' : 'Execution failed'
  });
});

Peripheral Communication

Communicate with peripheral twins (printers, sensors, displays, etc.). Peripheral emit supports callbacks since it's targeted communication:

// Get a peripheral
const display = await instance.getPeripheralInstance('display-001');

// Send command (action) - peripheral.emit with callback
try {
  const result = await display.emit('SET_BRIGHTNESS', {
    level: 80
  }, () => {});
  console.log('Display updated:', result.status);
} catch (error) {
  console.error('Update failed:', error.message);
}

// Fire-and-forget to peripheral
display.emit('LOG_EVENT', { event: 'button_pressed' });

// Listen for peripheral events
display.on('buttonPressed', (data) => {
  console.log('Button pressed:', data.buttonId);
});

Twin Properties

In addition to events and actions, you can update and observe twin properties:

// Update reported properties (what the twin is)
await instance.updateReported({
  status: 'online',
  firmware: '1.2.3'
});

// Update desired properties (what the twin should do)
await peripheral.updateDesired({
  brightness: 100,
  volume: 50
});

// Listen for property updates
peripheral.onUpdateDesired((desired) => {
  console.log('New desired state:', desired);
  // Apply the desired configuration
});

peripheral.onUpdateReported((reported) => {
  console.log('Peripheral reported:', reported);
});

Instance API Reference

MethodDescription
emit(type, data)Broadcast event to subscribers (fire-and-forget)
to(twinId).emit(type, data)Send event to specific twin (fire-and-forget)
to(twinId).emit(type, data, cb)Send action to specific twin (returns Promise)
on(type, callback)Listen for events/actions
updateReported(props)Update reported properties
updateDesired(props)Update desired properties
onUpdateReported(cb)Listen for reported changes
onUpdateDesired(cb)Listen for desired changes

Peripheral API Reference

MethodDescription
emit(type, data)Send event to peripheral (fire-and-forget)
emit(type, data, cb)Send action to peripheral (returns Promise)
on(type, callback)Listen for peripheral events/actions
to(twinId).emit(...)Send to a different twin

When to Use Twin Messaging

Twin Messaging is ideal when:

  • Messages need to work across any network (VPNs, firewalls, cellular)
  • You need reliable delivery through the hub
  • Latency of 50-200ms is acceptable
  • You're sending commands, status updates, or configuration

Consider Real-Time Channels when:

  • You need sub-10ms latency
  • You're streaming video or audio
  • You're sending high-frequency sensor data (100+ messages/sec)
  • You're transferring large binary data

Checkpoint

You should now understand:

  • The difference between Events and Actions
  • When to use emit() vs to().emit()
  • How to send and receive messages using the Instance API
  • Common communication patterns between twins

Next Steps

© 2026 · PhyStack. An Ombori company