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:

Property
Type
Description

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:

Method
When to Use

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., ReplaceCurrent policy receives a new batch)

  • The action's TimeoutSeconds expires

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.

1

Create the Script

Create a new C# file named HighlightObjectExecutor.cs in your project:

2

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).

3

Wire It to an Action Definition

In ConvaiActionConfigSource on the same NPC GameObject:

  1. Add a new entry in Action Definitions.

  2. Set Action Name to Highlight (or whatever name you use).

  3. Set Target Requirement to Object.

  4. Drag HighlightObjectExecutor into the Executor field.

4

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.

Executors run on the Unity main thread. You can safely access transform, GetComponent, Instantiate, and other Unity APIs without marshaling.

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?