Implement a custom module
Build a custom runtime module that starts with the SDK, accesses its services, and reacts to character and session events.
Build a custom module that integrates with the Convai runtime lifecycle, accesses SDK services, and subscribes to domain events. Before starting, read Runtime module system to understand when a module is the right tool and how the lifecycle states map to your implementation.
Prerequisites
A working Convai scene with a
ConvaiManagercomponentC# proficiency, including async/await and Unity's
MonoBehaviourlifecycleFamiliarity with the Runtime module system
Quickstart: minimal module
Before reading the full interface contract, here is the shortest path to a working module — a MonoBehaviour that registers itself and subscribes to one SDK event:
// MinimalModule.cs
using System;
using System.Collections.Generic;
using System.Threading;
using Convai.Domain.DomainEvents.Runtime;
using Convai.Domain.EventSystem;
using Convai.Runtime.Components;
using Convai.Runtime.Core.Modules;
using UnityEngine;
public class MinimalModule : MonoBehaviour, IConvaiModule
{
public string ModuleId => "my-project.minimal";
public string DisplayName => "Minimal Module";
public IReadOnlyList<string> RequiredModules => Array.Empty<string>();
public IReadOnlyList<Type> RequiredServices => Array.Empty<Type>();
public IReadOnlyList<Type> ProvidedServices => Array.Empty<Type>();
public bool IsActive { get; private set; }
private IEventHub _events;
private SubscriptionToken _sub;
private void Awake() => ConvaiManager.ActiveManager?.RegisterModule(this);
private void OnDestroy() => ConvaiManager.ActiveManager?.UnregisterModule(this);
public System.Threading.Tasks.ValueTask RegisterAsync(IModuleContext ctx, CancellationToken ct = default)
{
_events = ctx.Events;
return System.Threading.Tasks.ValueTask.CompletedTask;
}
public System.Threading.Tasks.ValueTask StartAsync(IModuleContext ctx, CancellationToken ct = default)
{
_sub = ctx.Events.Subscribe<CharacterSpeechStateChanged>(e =>
{
if (e.IsSpeaking) Debug.Log($"[MinimalModule] Character {e.CharacterId} started speaking.");
});
IsActive = true;
return System.Threading.Tasks.ValueTask.CompletedTask;
}
public System.Threading.Tasks.ValueTask PauseAsync(RuntimePauseReason r, CancellationToken ct = default)
{ IsActive = false; return System.Threading.Tasks.ValueTask.CompletedTask; }
public System.Threading.Tasks.ValueTask ResumeAsync(CancellationToken ct = default)
{ IsActive = true; return System.Threading.Tasks.ValueTask.CompletedTask; }
public System.Threading.Tasks.ValueTask StopAsync(CancellationToken ct = default)
{ _events?.Unsubscribe(_sub); IsActive = false; return System.Threading.Tasks.ValueTask.CompletedTask; }
}Add this component to any GameObject in the scene. The full interface contract and advanced patterns follow below.
IConvaiModule interface
Lifecycle method reference
RegisterAsync
During runtime build — before any StartAsync
Register services via context.ProvideModuleService<T>(). Subscribe to events.
StartAsync
Runtime start — after all modules are registered
Start active behaviors: begin processing, initialize hardware, start coroutines.
PauseAsync
Runtime paused (app loses focus, deliberate pause)
Stop processing. Use RuntimePauseReason to distinguish why.
ResumeAsync
Runtime resumed
Restart processing paused in PauseAsync.
StopAsync
Runtime stopping or module removed
Clean up: unsubscribe, stop coroutines, release resources.
IModuleContext services
Runtime
ConvaiRuntime
Always
The runtime instance this module belongs to.
Events
IEventHub
Always
Publish and subscribe to domain events.
Agents
IAgentRegistry
Always
Query registered characters and players.
Transport
ITransportProvider
May be null
Platform-specific communication layer.
Preferences
IRuntimePreferences
May be null
Mutable runtime preferences.
Logger
ILogger
May be null
Logger for diagnostics.
RoomAudio
IConvaiRoomAudioService
May be null
Microphone and playback service.
Credentials
ICredentialProvider
May be null
API key and server URL resolution.
Always null-check Transport, Preferences, Logger, RoomAudio, and Credentials before use. Events and Agents are guaranteed to be non-null. Accessing a null service throws a NullReferenceException that halts the module's lifecycle.
For the full list of subscribable domain events, see Event System.
Implement a module
Event subscriber example
A module that subscribes to a domain event and triggers haptic feedback when a character speaks.
Service provider and consumer example
A module declares its provided services in ProvidedServices, registers the instance in RegisterAsync, and consuming modules retrieve it via TryGetModuleService<T>.
A consuming module:
Always use TryGetModuleService — never assume the service is present. If AudioAnalysisModule is not registered, TryGetModuleService returns false without throwing, letting VisualizerModule degrade gracefully.
Register a module
MonoBehaviour self-registration (recommended)
Attach the module as a component to any GameObject. It self-registers with ConvaiManager on Awake.
After ConvaiManager.Start() completes, ConvaiManager.ActiveManager.IsInitialized returns true, indicating all registered modules have been discovered and the runtime has started.
CreateRuntimeBuilder override
Use this when you prefer all customization in one place, or when the module is not a MonoBehaviour.
Use the dependency injection pattern
Components on ConvaiCharacter or ConvaiPlayer GameObjects can receive SDK services automatically by implementing IInjectable<TDependencies>. The SDK injects dependencies after the character or player is registered with the runtime.
IInjectable<TDependencies>
IConvaiCharacterDependencies
EventHub
IEventHub
Required
ConnectionService
IConvaiRoomConnectionService
Required
AudioService
IConvaiRoomAudioService
Required
AgentRegistry
IAgentRegistry
Optional
Logger
ILogger
Optional
IConvaiPlayerDependencies
PlayerInputService
IPlayerInputService
Optional
RuntimeSettingsService
IConvaiRuntimeSettingsService
Optional
Logger
ILogger
Optional
Write an injectable component
Add this component to the same GameObject as ConvaiCharacter. The SDK calls InjectDependencies automatically during character registration.
Usage examples
Example 1: Biometric correlation module for medical simulation
Records character emotion data alongside biometric sensor readings for post-session analysis.
Example 2: Assessment scoring module for industrial training
Tracks character-triggered actions against a scoring rubric and exposes the score service to other modules via ProvideModuleService.
Troubleshooting
Module's StartAsync never called
RegisterAsync threw an unhandled exception; the runtime halts module startup silently
Wrap RegisterAsync body in a try-catch and log explicitly.
TryGetModuleService<T> returns false unexpectedly
ProvideModuleService<T> was called in StartAsync instead of RegisterAsync
Move ProvideModuleService<T> to RegisterAsync — services must be registered before any module's StartAsync runs.
Module starts but misses early events
Subscribed in RegisterAsync but event fires during startup before StartAsync
Move subscriptions to StartAsync, or guard with IsActive check in the handler.
RequiredModules entry causes startup error
Listed module ID not registered before runtime build
Verify the module ID string matches exactly — IDs are case-sensitive.
InjectDependencies never called on IInjectable component
Component is not on a GameObject in the ConvaiCharacter hierarchy
IInjectable<IConvaiCharacterDependencies> only works on GameObjects that are children of a character.
ConvaiManager.ActiveManager is null in Awake
Manager's Awake has not yet run at execution order −1100
Register modules in Start() or use ConvaiManager.ActiveManager?.RegisterModule(this) with null-safety.
Next steps
Logging, metrics, and retry policyEvent systemLast updated
Was this helpful?