Writing Custom Executors
Implement IConvaiActionExecutor to create any behavior your project requires — with full examples, cancellation handling, and compound action patterns.
When to Write a Custom Executor
The executors included with the Convai SDK cover common scenarios, but every project has unique behavior requirements. Write a custom executor when:
Your game uses a custom movement system (character controller, physics, pathfinding library)
You need to interact with a UI system, inventory, physics object, or external service
You want to combine multiple behaviors with conditional logic
The built-in executors don't match the exact behavior your NPC needs
Custom executors are just C# classes — a MonoBehaviour that implements one interface. There is no SDK-specific boilerplate beyond that.
The IConvaiActionExecutor Interface
Any MonoBehaviour that implements IConvaiActionExecutor can be used as an executor.
using System.Threading;
using System.Threading.Tasks;
using Convai.Runtime.Actions;
public interface IConvaiActionExecutor
{
Task<ConvaiActionExecutionResult> ExecuteAsync(
ConvaiActionInvocation invocation,
CancellationToken cancellationToken);
}The method is async by design. You can await coroutines, tasks, or any asynchronous operation inside it.
The Invocation Object
ConvaiActionInvocation is passed to ExecuteAsync and contains everything you need to run the behavior:
Command
ConvaiActionCommand
The raw command from the backend: Command.Name and Command.Target (the unresolved target name string).
Definition
ConvaiActionDefinition
Your local action definition: Definition.ActionName, Definition.TimeoutSeconds, etc.
ResolvedTarget
ConvaiResolvedActionTarget
The matched target. ResolvedTarget.GameObjectReference gives you the scene GameObject.
Character
ConvaiCharacter
The character running this action.
BatchIndex
int
Which batch this invocation belongs to (0-based).
StepIndex
int
Which step within the batch this is (0-based).
To get the target's scene GameObject:
Returning a Result
Return one of the following factory methods from ConvaiActionExecutionResult:
ConvaiActionExecutionResult.Succeeded()
The action completed successfully.
ConvaiActionExecutionResult.Failed(string message, Exception ex)
Something went wrong (missing component, invalid state, etc.). Message and exception are optional.
ConvaiActionExecutionResult.Unhandled(string message)
This executor cannot handle the given invocation (e.g., wrong target type). The dispatcher fires OnStepUnhandled. Message is optional.
ConvaiActionExecutionResult.Canceled()
The action was canceled (typically because you detected cancellationToken.IsCancellationRequested).
ConvaiActionExecutionResult.TimedOut()
Returned automatically by the dispatcher when TimeoutSeconds is exceeded — you do not need to return this manually.
Cancellation and Timeouts
ExecuteAsync receives a CancellationToken. This token is canceled when:
The batch is canceled (e.g.,
ReplaceCurrentpolicy receives a new batch)The action's
TimeoutSecondsexpires
If your executor runs a loop or awaits a long-running operation, check the token to avoid blocking indefinitely:
Or use the token with await directly — the OperationCanceledException is caught by the dispatcher automatically:
Step-by-Step: Build a "Highlight Object" Executor
This example builds a HighlightObjectExecutor that enables an outline/highlight component on the target object when triggered, waits three seconds, then disables it.
Create the Script
Create a new C# file named HighlightObjectExecutor.cs in your project:
Add the Component to Your NPC
Select your NPC's GameObject and click Add Component → My Game → Highlight Object Executor.
Set Highlight Duration to your desired value (default: 3 seconds).
Wire It to an Action Definition
In ConvaiActionConfigSource on the same NPC GameObject:
Add a new entry in Action Definitions.
Set Action Name to
Highlight(or whatever name you use).Set Target Requirement to
Object.Drag
HighlightObjectExecutorinto the Executor field.
Test It
Press Play and say to the character:
"Highlight the fire extinguisher."
The Outline component on the fire extinguisher should enable for three seconds, then disable.
A Simpler Example: Teleport Executor
For reference, here is the minimal custom executor pattern — no async waiting, just a synchronous action:
Note the use of Task.FromResult — for synchronous executors, wrap the result rather than using async/await.
Compound Executor Pattern
For actions that consist of multiple gameplay steps, put the entire sequence inside one ExecuteAsync. The dispatcher treats one action definition as one indivisible unit — it waits for your task to complete before starting the next step.
See PickUpActionExecutor in the SDK for a complete reference implementation of this pattern.
Tips and Best Practices
Return Unhandled when this executor is not the right one for the job. For example, if you have an executor that only handles living targets and receives an object target, return Unhandled rather than Failed. This allows other executors (or a fallback) to handle the step without it counting as a failure.
Return Failed for genuine errors — missing components, null references, invalid state. Include a descriptive message so the debug probe and console logs help you diagnose problems.
Always check the CancellationToken in any loop or long await. An unchecked loop inside ExecuteAsync will block the dispatcher and prevent new batches from running, even after a policy cancels the current batch.
Executors run on the Unity main thread. You can safely access transform, GetComponent, Instantiate, and other Unity APIs without marshaling.
Use invocation.ResolvedTarget and invocation.Definition — do not re-parse invocation.Command.Name or Command.Target manually. Resolution and normalization have already been done for you. Parsing the raw strings yourself bypasses case-insensitive matching and leads to brittle code.
Keep the task alive for as long as the gameplay work is running. The dispatcher waits for ExecuteAsync to return before starting the next step. For long-running actions like pathfinding, keep the loop running inside the executor until the work is actually done.
Conclusion
A custom executor is just a MonoBehaviour with one method. Implement IConvaiActionExecutor, get the target from invocation.ResolvedTarget.GameObjectReference, do your work, and return a result. For long-running behaviors, use async/await and check the CancellationToken to respect timeout and policy cancellation.
Last updated
Was this helpful?