Write a custom action executor
Implement IConvaiActionExecutor on a MonoBehaviour to connect custom movement, inventory, UI, or physics behaviors to the Convai action pipeline.
When the built-in executors don't match your project's movement system, interaction model, or gameplay rules, implement IConvaiActionExecutor. A custom executor is a standard C# MonoBehaviour with a single async method. The dispatcher treats it identically to any built-in executor — all policies, events, and cancellation behavior apply automatically.
When to build a custom executor
Build a custom executor when:
Your project uses a custom movement system (root motion,
CharacterController, steering behaviors)An action modifies inventory, UI state, quest flags, or physics objects
An action calls an external service or triggers a coroutine-based animation system
You need conditional logic — for example, an action that behaves differently depending on character state
The IConvaiActionExecutor interface
public interface IConvaiActionExecutor
{
Task<ConvaiActionExecutionResult> ExecuteAsync(
ConvaiActionInvocation invocation,
CancellationToken cancellationToken);
}Implement this interface on any MonoBehaviour. The dispatcher calls ExecuteAsync for each step and awaits the result before proceeding to the next step. Keep your task alive until the gameplay work is complete — returning early ends the step, even if the animation or movement is still running.
Executors run on Unity's main thread. You can safely call Unity APIs (transform, GetComponent, Instantiate, etc.) anywhere in ExecuteAsync. Use await Task.Yield() to yield a frame without leaving the main thread.
The ConvaiActionInvocation object
Every ExecuteAsync call receives a ConvaiActionInvocation with everything needed to perform the behavior:
Command
ConvaiActionCommand
Raw backend command — Name, Target, HasTarget
Definition
ConvaiActionDefinition
Local definition — ActionName, TargetRequirement, Executor, TimeoutSeconds
ResolvedTarget
ConvaiResolvedActionTarget
Resolved target binding — Kind, Name, ObjectBinding, CharacterBinding, GameObjectReference
Character
ConvaiCharacter
The executing NPC
BatchIndex
int
Sequential index of this batch across the dispatcher's lifetime
StepIndex
int
Index of this step within the current batch (0-based)
Access the target GameObject with:
Do not re-parse invocation.Command.Name or invocation.Command.Target to re-derive what to do. Use invocation.Definition and invocation.ResolvedTarget — they are already resolved and validated.
Execution result types
Return one of these factory methods from ExecuteAsync:
ConvaiActionExecutionResult.Succeeded()
The behavior completed successfully
ConvaiActionExecutionResult.Failed(string message = null, Exception exception = null)
A genuine error occurred (missing component, invalid state, gameplay failure)
ConvaiActionExecutionResult.Unhandled(string message = null)
This executor intentionally declines to handle the invocation (wrong context or target type)
ConvaiActionExecutionResult.Canceled()
The CancellationToken was signaled — return this when you observe cancellation in a loop
Do not return ConvaiActionExecutionResult.TimedOut() manually. The dispatcher returns TimedOut automatically when TimeoutSeconds expires and the CancellationToken is triggered. If you return it yourself, the result is ambiguous and the dispatcher's timeout tracking is bypassed.
Failed vs Unhandled: Use Failed when you tried to perform the behavior and something went wrong. Use Unhandled when this executor should not handle this particular invocation at all — for example, if the target is the wrong type. The dispatcher fires OnStepFailed for Failed and OnStepUnhandled for Unhandled; both are treated as non-success for the StopBatch failure policy.
Cancellation
The CancellationToken is triggered when:
BatchPolicy.ReplaceCurrentactivates (a new batch preempts the current one)TimeoutSecondson the action definition expiresThe dispatcher is disabled or destroyed
Always check the token in any loop or after each await:
If your code catches OperationCanceledException, return ConvaiActionExecutionResult.Canceled() immediately:
Alternatively, let ThrowIfCancellationRequested propagate. The dispatcher wraps your ExecuteAsync in a try/catch and converts uncaught OperationCanceledException to Canceled automatically.
Complete example: highlight object executor
This executor enables an outline effect on the resolved target, waits three seconds, then disables it.
Compound actions
Put the entire gameplay sequence inside one ExecuteAsync. The dispatcher treats one action definition as indivisible — it waits for your task to complete before starting the next step. This is the correct pattern for actions like pick-up, inspect, open-then-take, or any sequence that involves multiple sub-behaviors.
Executor design rules
Use
invocation.ResolvedTarget, notinvocation.Command.Target. The dispatcher has already resolved the name to aGameObjectbinding — don't re-parse the raw string.Return
Unhandledwhen this executor is not appropriate. A single executor component can be shared across multiple action definitions. ReturningUnhandledsignals the dispatcher to fireOnStepUnhandledwithout treating it as a hard failure.Set
TimeoutSecondsin the action definition. Use the timeout mechanism rather than implementing your own deadline logic inside the executor.Clean up on cancellation. If your executor enables an effect, moves an object, or holds a resource, release it before returning
Canceled.Do not hold state between invocations. The same executor instance may be called for different targets across multiple batches. Do not assume the previous invocation's state is still valid.
Next steps
Configure character actionsCharacter actions scripting referenceLast updated
Was this helpful?