Custom Actions¶
A custom action is a tool the AI can choose to call, implemented by your script. The AI decides when (from the player's words and your description); your script does the actual work and reports back. This is how an NPC can change lights, open a door, hand out an item, start a minigame — anything you can script.
This is the opposite direction from World Events:
- Custom Action = the AI → your script. "Do this." (you implement it)
- World Event = your script → the NPC. "This happened." (
NPC.Notify)
1. Write the handler¶
A custom action is just a public method on a NexusBehaviour that takes the call's arguments as a JSON string:
using UnityEngine;
using NexusVM.Unity;
class RoomLights : NexusBehaviour
{
public Light lamp;
public void OnSetLightColor(string paramsJson)
{
var p = Json.FromJsonToDict(paramsJson);
string color = ("" + p["color"]).ToLower();
if (lamp == null) { NPC.CommandError("No lamp is set up."); return; }
if (color == "blue") lamp.color = Color.blue;
else if (color == "red") lamp.color = Color.red;
else { NPC.CommandError("I can't do the color '" + color + "'."); return; }
NPC.CommandResult("The lamp is now " + color); // success message the AI can speak
}
}
Json.FromJsonToDict(paramsJson)parses the AI's arguments.NPC.CommandResult(msg)reports success — the AI usesmsgto confirm to the player.NPC.CommandError(msg)reports failure — the AI explains it naturally instead of pretending it worked.
2. Register it (Action Designer ▸ Actions)¶
On the NPC's SSAINpc, open Action Designer ▸ Actions ▸ ➕ Add Action ▸ Custom Script, and set:
| Field | Example |
|---|---|
| Command ID | set_light_color (what the AI calls) |
| Script function | OnSetLightColor (your method) |
| AI description | "Change the room light's color. Use when the player asks." |
| Parameters | color (string) — described so the AI fills it correctly |
The description and parameters are what the AI sees — write them like you're telling an assistant what the tool does and what to pass.
Built‑in actions, no script needed
➕ Add Action also lists ready‑made built‑ins — move to player, look at, play a sound, emote, set a light — pre‑filled. Use those for common things; reach for a custom script when you need your own logic.
3. Put the script where the NPC can reach it¶
The handler must be findable by the NPC. Either:
- put the script's NexusComponent on the NPC's GameObject, or
- add it to the NPC's Action Scripts list (so a script on another object can provide the action).
Parameters in detail¶
Each parameter you declare can be exposed to the AI (the AI fills it) or fixed (a design‑time value the AI never controls — handy for scene references). Fixed values are merged in for you before your handler runs, so paramsJson always has everything.
Who triggered the action¶
When the action is about the person talking — "what's my score", "save my progress" — get the asker from the current speaker, not from a parameter (never trust the AI with an identity):
string uid = Player.GetSpeakerUserId(); // server-verified account id of whoever is asking
Player.GetSpeakerId() / GetSpeakerUserId() are set by the server for the message the NPC is handling — see Player API ▸ Who's talking. Pair it with HTTP / Supabase to fetch that player's row.
Always report a result¶
Call NPC.CommandResult (or CommandError) at the end of every path. If you don't, the NPC may say a vague "Done" — or, worse, claim success on a failure. The message you pass is the AI's source of truth for what actually happened.
→ Full multi‑action example: RoomLightsBehaviour.cs (color, RGB, power).
Async actions (await a web request)¶
A normal action returns immediately. But if your action makes an HTTP / Supabase request — which finishes later, from a coroutine — tick Async (awaits a web request) on the action (Action Designer ▸ Actions). The NPC then waits for your NPC.CommandResult and works the fetched value into its reply in the same turn:
// Action "get_score", marked ✔ Async
public void OnGetScore(string paramsJson)
{
string uid = Player.GetSpeakerUserId();
if (uid == "") { NPC.CommandError("I can't tell who's asking."); return; }
StartCoroutine(FetchScore(uid)); // no result yet — the coroutine reports it
}
IEnumerator FetchScore(string uid)
{
var r = HTTP.GetVia("scores-db", "/rest/v1/scores?select=score&user_id=eq." + uid);
yield return r; // wait for the request
if (r.Success) NPC.CommandResult("Their score is " + ("" + Json.FromJsonToDict(r.Body)["score"]) + " points.");
else NPC.CommandError("The leaderboard is unreachable right now.");
}
→ Full example: ScoreboardNpc.cs — and ➕ Add Action ▸ Custom Script has a ready‑made "Example — Get player's score (async HTTP)" that pre‑fills this action (drop ScoreboardNpc's NexusComponent on the NPC to supply the handler). You have multiple paths for "fetch then answer":
| Want | Do |
|---|---|
| Answer in one turn (recommended) | Mark the action Async; NPC.CommandResult from the coroutine. |
| A two‑beat "let me check… → here it is" | Leave it sync; NPC.Notify("event", …) from the coroutine + react via a world event. |
| The NPC should always know it (no asking) | A context probe (below). |
Async is only for web requests
Leave it off for everything else. The NPC waits up to 30s for an async action to report, so an async action must call NPC.CommandResult/CommandError on every path.
Context probes (ambient world state)¶
A context probe is a function that returns a string, run every turn and injected into the NPC's prompt under a [Current World State] header — so the NPC simply knows it, with no action call. Use it for state the NPC should always be aware of (who's nearby, the score on a board, the time of day).
public string WorldState()
{
string[] ids = Networking.GetPlayers();
return ids.Length + " players are here right now.";
}
Wire it in Action Designer ▸ Context Probes ▸ ➕ Add Probe — which now offers ready‑made examples (e.g. "Who's nearby (WorldState)") as well as a blank probe. Each is a label (defaults to "World state") + a function name. A probe returns its string — it does not call NPC.CommandResult (that's for actions). Keep probes cheap: they run on every message. → Example: AreaProbe.cs shows a probe and an on‑demand scan_area action side by side.