WorldState and the scripting interface

WorldState is the sole input to an agent’s decisions. This chapter covers its value types, C++ read/write, script (Lua / ScriptCanvas) read/write, and the completion callback contract for script operators.

Value type HtnValue

HtnValue is a closed eight-type variant (public header HtnPlanner/HtnValue.h). Closed on purpose: the planner must be able to compare and simulate every value, the editor must be able to edit it, and serialization must round-trip.

using HtnValue = AZStd::variant<
 bool, // Bool
 int64_t, // Int
 float, // Float
 AZ::Vector3, // Vector3
 AZ::EntityId, // EntityId
 AZ::Crc32, // Tag: authored as a string, Crc'd at compile time
 AZ::Uuid, // Uuid: asset / external object references
 AZStd::string>; // String: human-readable text (names, labels, dialogue ids)

The matching type enum HtnValueType: Bool / Int / Float / Vector3 / EntityId / Tag / Uuid / String.

TypeComparableOrderableArithmetic (Add/Sub)Typical use
BoolyesnonoToggle facts (EnemyVisible)
IntyesyesyesCounters (Ammo, Health)
FloatyesyesyesContinuous quantities (equality is exact; determinism forbids epsilon tolerance)
Vector3yesnonoPositions, directions
EntityIdyesnonoTarget entity references
TagyesnonoEnum-like labels (string Crc; compact and fast to compare)
UuidyesnonoAsset references
StringyesnonoWhen the literal text matters (dialogue ids, etc.)

Use AZStd::get<T>(value) to access a known type; the same header also offers helpers such as GetValueType / MakeDefaultValue / ValuesEqual / CompareValues / AddValues / SubtractValues / ValueToString.

Reading and writing WorldState from C++

Go through HtnAgentRequestBus, addressed by key name (AZ::Crc32). A type mismatch logs a warning and returns false (no crash):

#include <HtnPlanner/HtnPlannerBus.h>
#include <HtnPlanner/HtnValue.h>

// Write: a sensor reports an enemy sighting
HtnPlanner::HtnAgentRequestBus::Event(
 agentId, &HtnPlanner::HtnAgentRequests::SetWorldStateValue,
 AZ::Crc32("Guard.EnemyVisible"), HtnPlanner::HtnValue(true));

// Write: update the ammo count (Int uses int64_t)
HtnPlanner::HtnAgentRequestBus::Event(
 agentId, &HtnPlanner::HtnAgentRequests::SetWorldStateValue,
 AZ::Crc32("Guard.Ammo"), HtnPlanner::HtnValue(int64_t{ 12 }));

// Read: fetch a key's current value
AZStd::optional<HtnPlanner::HtnValue> ammo;
HtnPlanner::HtnAgentRequestBus::EventResult(
 ammo, agentId, &HtnPlanner::HtnAgentRequests::GetWorldStateValue, AZ::Crc32("Guard.Ammo"));
if (ammo.has_value())
{
 const int64_t value = AZStd::get<int64_t>(ammo.value());
 // .
}

Key points:

  • Keys must be declared in the Domain (WorldState Key nodes); writes to undeclared keys fail with a warning. Key types are locked at compile time.
  • Writes affect only that agent’s WorldState; HTN does not replicate WorldState over the network (in multiplayer the authority usually owns the agent and runs planning).
  • Every successful write bumps the revision—the trigger for opportunistic replan.

Scripting (Lua / ScriptCanvas)

The system registers a set of global functions in BehaviorContext under the HTN category:

Global functionEquivalent EBus call
HtnRequestPlan(entityId)RequestPlan
HtnRequestReplan(entityId)RequestReplan
HtnStop(entityId, abortCurrentOperator)Stop
HtnIsExecuting(entityId)IsExecuting
HtnGetCurrentStepName(entityId)GetCurrentStepName
HtnSetWorldState(entityId, key, value)SetWorldStateValue
HtnGetWorldState(entityId, key)GetWorldStateValue
HtnCompleteCurrentOperator(entityId, token, success)CompleteCurrentOperator

Lua example:

-- Write a bool fact and request a replan
HtnSetWorldState(self.entityId, "Guard.EnemyVisible", true)
HtnRequestReplan(self.entityId)

Value boxing at the script boundary

Arguments cross the script boundary boxed as AZStd::any. Script numbers widen as usual (double → Float, integers → Int). The key pitfall: by default, a string passed from script is boxed as a Tag (Crc32), not a String.

  • If your key really is Tag-typed, just pass the string.
  • If the key must preserve the literal text, declare the key as String in the Domain—the write path then keeps the string literal based on the key’s expected type.

Script operator completion callbacks

When a primitive task binds the built-in bridge operator Script.SendOperatorEvent, the operator body is implemented in script (bus definitions in the public header HtnPlanner/HtnScriptOperatorBus.h):

sequenceDiagram participant R as HtnPlanRunner participant S as Script (Lua/ScriptCanvas) R->>S: OnOperatorBegin(operatorName, token, args) Note over S: operator stays Running R->>S: OnOperatorTick(operatorName, token) every frame S->>R: HtnCompleteCurrentOperator(entityId, token, success) Note over R: a token that does not match the in-flight generation is ignored with a warning

HtnScriptOperatorNotificationBus (addressed by agent entity id, reflected to BehaviorContext):

virtual void OnOperatorBegin(const AZStd::string& operatorName, uint32_t operationToken,
 const AZStd::vector<AZStd::any>& args);
virtual void OnOperatorTick(const AZStd::string& operatorName, uint32_t operationToken);
virtual void OnOperatorAborted(const AZStd::string& operatorName, uint32_t operationToken);
virtual void OnHtnEvent(const AZStd::string& eventName, const AZStd::vector<AZStd::any>& args); // one-shot notification from SendScriptEvent

Lua implementation sketch:

function MyOperatorHandler:OnOperatorBegin(operatorName, token, args)
 self.token = token -- remember the generation token
 -- kick off an async action (door animation, movement, .)
end

-- Report completion when the async action finishes; must pass back the same token
function MyOperatorHandler:OnAnimFinished()
 HtnCompleteCurrentOperator(self.entityId, self.token, true)
end

The token is a “generation token”: if a replan happened in between, the old operator was Aborted and completion reports with the stale token are ignored (with a warning), filtering out late callbacks from async actions. Async script operators must save and return the token received in Begin, or the operator stays Running forever.

The operatorName for Script.SendOperatorEvent comes from the operatorName setting of the .htnnode preset that wraps it; see Extending operators for how to bind script operators in the graph.


Copyright © 2026 DawnEngine. All rights reserved.

DawnEngine is a commercial 3D engine distributed under the DawnEngine end-user license agreement. Engine binaries and source are proprietary and are not covered by the licenses below.

Documentation only: the prose and templates on this site are a derivative work of Open 3D Engine (O3DE) documentation by the O3DE Contributors, used under CC BY 4.0 (documentation content), Apache 2.0 (site code), and the MIT license (inline code samples).

The open-source 3D engine that DawnEngine is built on top of is Open 3D Engine . DawnEngine is not affiliated with, endorsed by, or sponsored by The Linux Foundation or the O3DE project. “O3DE” and “Open 3D Engine” are trademarks of The Linux Foundation.