Skip to main content

What is Discovery?

Discovery is the system that automatically makes functions and triggers available across your entire backend application stack without manual configuration. When a worker connects and registers capabilities, they become immediately available to all other parts of the system.
Discovery eliminates the need for service registries, API gateways, or configuration files. Everything is registered dynamically over the WebSocket protocol.

How Discovery Works

The discovery process follows this flow:
  1. Worker connects to the engine via WebSocket
  2. Worker registers its functions and trigger types
  3. Engine tracks all registrations in thread-safe registries
  4. Functions become callable by any other worker or module
  5. Worker disconnects, all its registrations are cleaned up

Worker Registration

When a worker first connects, the engine assigns it a unique ID and tracks it in the WorkerRegistry:
pub struct Worker {
    pub id: Uuid,
    pub channel: mpsc::Sender<Outbound>,
    pub function_ids: Arc<RwLock<HashSet<String>>>,
    pub external_function_ids: Arc<RwLock<HashSet<String>>>,
    pub invocations: Arc<RwLock<HashSet<Uuid>>>,
    pub runtime: Option<String>,
    pub version: Option<String>,
    pub connected_at: DateTime<Utc>,
    pub ip_address: Option<String>,
    pub status: WorkerStatus,
}

Worker Lifecycle

1

Connection

Worker establishes WebSocket connection to engine (default port 49134)
const iii = init('ws://localhost:49134');
await iii.connect();
2

Worker ID Assignment

Engine generates a UUID and sends WorkerRegistered message:
{
  "type": "workerregistered",
  "worker_id": "550e8400-e29b-41d4-a716-446655440000"
}
3

Capability Registration

Worker registers its functions and trigger types:
// Functions become discoverable
iii.registerFunction({ id: 'users.create' }, handler);

// Trigger types become available
iii.registerTriggerType({ id: 'http', description: 'HTTP routes' });
4

Ready for Work

Worker is now fully integrated and can:
  • Execute functions registered by other workers
  • Have its own functions called by others
  • Register triggers that invoke any function

Function Discovery

Functions are stored in the FunctionsRegistry, a concurrent hash map that allows lock-free lookups:
pub struct FunctionsRegistry {
    pub functions: Arc<DashMap<String, Function>>,
}

impl FunctionsRegistry {
    pub fn register_function(&self, function_id: String, function: Function) {
        tracing::info!("[REGISTERED] Function {}", function_id);
        self.functions.insert(function_id, function);
    }
    
    pub fn get(&self, function_id: &str) -> Option<Function> {
        self.functions.get(function_id).map(|entry| entry.value().clone())
    }
}

Function Registration Protocol

{
  "type": "registerfunction",
  "id": "users.create",
  "description": "Create a new user",
  "request_format": {
    "email": { "type": "string" },
    "name": { "type": "string" }
  },
  "response_format": {
    "userId": { "type": "string" }
  },
  "metadata": null,
  "invocation": null
}
External functions are special: they’re stored in external_function_ids and invoke HTTP endpoints instead of local handlers. The engine manages the HTTP lifecycle transparently.

Trigger Discovery

Triggers have a two-tier discovery system:

1. Trigger Type Registration

Workers first declare what trigger types they support:
// A worker declares it can handle HTTP triggers
iii.registerTriggerType({
  id: 'http',
  description: 'HTTP API routes'
});
This gets stored in the TriggerRegistry:
pub struct TriggerRegistry {
    pub trigger_types: Arc<DashMap<String, TriggerType>>,
    pub triggers: Arc<DashMap<String, Trigger>>,
}

2. Trigger Instance Registration

Once a trigger type exists, any worker can register trigger instances:
// Any worker can now create HTTP triggers
iii.registerTrigger({
  type: 'http',           // Must match registered type
  function_id: 'users.create',  // Can be ANY function
  config: {
    api_path: 'users',
    http_method: 'POST'
  }
});
If you register a trigger before its type exists, the trigger is stored but not activated. It activates automatically when the trigger type is registered later.

Cross-Runtime Discovery

Discovery works seamlessly across different runtimes. A Python worker can call functions registered by a Node.js worker, and vice versa:
# Python worker
iii.register_function("python.process", process_data)
// Node.js worker - can immediately call python.process
const result = await iii.call('python.process', { data: [...] });
// Rust worker - can also call python.process
let result = iii.call("python.process", json!({ "data": [...] })).await?;

Automatic Cleanup

When a worker disconnects, the engine automatically cleans up all its registrations:
async fn cleanup_worker(&self, worker: &Worker) {
    // 1. Remove all regular functions
    let regular_functions = worker.get_regular_function_ids().await;
    for function_id in regular_functions.iter() {
        self.remove_function_from_engine(function_id);
    }
    
    // 2. Remove all external functions
    let external_functions = worker.get_external_function_ids().await;
    for function_id in external_functions.iter() {
        http_module.unregister_http_function(function_id).await?;
    }
    
    // 3. Halt all pending invocations
    let worker_invocations = worker.invocations.read().await;
    for invocation_id in worker_invocations.iter() {
        self.invocations.halt_invocation(invocation_id);
    }
    
    // 4. Unregister all triggers and trigger types
    self.trigger_registry.unregister_worker(&worker.id).await;
    
    // 5. Remove worker from registry
    self.worker_registry.unregister_worker(&worker.id);
}
This automatic cleanup ensures that your system stays consistent even when workers crash or disconnect unexpectedly.

Service Discovery

In addition to functions and triggers, workers can register services:
iii.registerService({
  id: 'user-service',
  name: 'User Management Service',
  description: 'Handles all user operations'
});
Services are logical groupings that help organize functions but don’t affect routing.

Discovery at Scale

The discovery system is designed for high concurrency:
  • Lock-free reads: Uses DashMap for concurrent hash map access
  • Async message passing: Workers communicate via async channels
  • Non-blocking registration: Functions become available immediately
  • Distributed tracing: All discovery operations are traced via OpenTelemetry

Performance Characteristics

Function Lookup

O(1) hash map lookup with zero lock contention

Registration

Instant - no coordination or consensus required

Worker Cleanup

Async - cleanup runs in background without blocking

Cross-worker Calls

Single WebSocket hop - direct routing via engine

Discovery Events

You can listen for discovery events to react to changes:
// Built-in trigger type for worker lifecycle
iii.registerTrigger({
  type: 'workers.available',
  function_id: 'monitor.workers',
  config: {}
});

iii.registerFunction({ id: 'monitor.workers' }, async (event) => {
  if (event.event === 'worker_connected') {
    console.log(`Worker ${event.worker_id} connected`);
  } else if (event.event === 'worker_disconnected') {
    console.log(`Worker ${event.worker_id} disconnected`);
  }
});

Health and Introspection

You can query the discovery state at runtime:
// List all registered functions
const functions = await iii.call('_internal.list_functions', {});

// List all workers
const workers = await iii.call('_internal.list_workers', {});

// Get worker details
const worker = await iii.call('_internal.get_worker', { 
  worker_id: '550e8400-e29b-41d4-a716-446655440000' 
});

Best Practices

Use namespaced IDs

Prefix functions with service name: users.create, orders.process

Register trigger types early

Register trigger types on worker startup before any triggers

Handle connection loss

Implement reconnection logic with exponential backoff

Monitor registrations

Track worker connection/disconnection events for observability

Next Steps

Architecture

Learn how the engine coordinates discovery across workers

Functions

Deep dive into function registration and invocation

Build docs developers (and LLMs) love