Cookbook

25 recipes for common AI design problems on top of Intent Forge — sensors, executors, anti-flap, debugging, dispatcher integration.

[ Cookbook ]

25 recipes for common AI problems

Tight, copy-pasteable recipes for the patterns you'll actually reach for in shipping code. All assume you've finished the Quickstart. Each recipe is a self-contained snippet. Skim the category cards below, or use the sidebar TOC to jump straight to a numbered recipe.

25 recipesCopy-pasteableCmd+F friendly

Browse by category


1. Flee at low health (emergencies that preempt mid-action)

Problem: when health drops below 20%, override whatever the agent is doing — even if it's mid-MoveTo, and run for cover.

Recipe: use a Critical interruption-level goal.

  1. Add Health (scalar, bTriggersReplan = true) to your fact schema.
  2. Author DA_Goal_Survive:
    • Desired State: Bool Fact EqualsIsSafe = true.
    • Considerations: Goal Consideration: Fact Score with a curve that returns >0 only when Health < 0.2.
    • InterruptionLevel: Critical.
  3. Author DA_Action_Flee with effect IsSafe = true.
  4. Because Critical outranks Normal regardless of score, the planner picks Flee as soon as the consideration starts returning a positive score. The MinActionCommitTime gate is automatically bypassed — the in-flight action gets CancelAction() immediately.

Why not just a high score? Score-based preemption is blocked by the min-commit gate for MinActionCommitTime seconds after every action starts. That's correct for normal goal-switching (prevents flapping), but wrong for emergencies. Interruption levels are the escape hatch.

Watch out for: every Critical goal raises the noise floor for every other goal. Reserve Critical for true emergencies (low HP, taking damage, ally calling for help). Use Urgent for important-but-not-critical priorities (low ammo, lost target). Normal covers everything else.


2. Prefer ranged attacks at distance, melee up close

Problem: same goal (TargetDestroyed), two ways to satisfy it.

Recipe:

  1. Schema: add DistanceToTarget (scalar, replan-triggering).
  2. Create two actions: DA_Action_RangedAttack and DA_Action_MeleeAttack. Both have effect TargetDestroyed = true.
  3. Give DA_Action_RangedAttack a UCostStrategy_FactWeighted:
    • BaseCost = 1.0
    • FactWeights = { DistanceToTarget: -0.01 } — cost goes down as distance goes up.
  4. Give DA_Action_MeleeAttack a fact-weighted cost that goes up with distance: FactWeights = { DistanceToTarget: 0.02 }.
  5. The planner picks whichever has lower projected cost given the current distance.

Why this works: UCostStrategy_FactWeighted evaluates against the current state during planning. Same goal, different costs by context.


3. Replan only on important events

Problem: the safety net replan every 0.75 s is wasteful — most ticks nothing has changed.

Recipe: this is already on by default. Replanning is gated on WorldState.StateVersion. The version only bumps when a fact actually changes. To take it further:

  1. Mark only meaningful facts with bTriggersReplan = true (the schema honors this implicitly via version-bumping).
  2. Raise ReplanSafetyNetInterval on the component to 2.0 or more.
  3. Lower Replan Check Tick Rate indirectly by setting PrimaryComponentTick.TickInterval to ~0.1 s in C++ if you need very long-running idle agents.

4. One-shot action that releases external resources

Problem: you wrote a custom executor that claims a Smart Object, spawns a particle system, or starts a timeline. If the plan is invalidated mid-step, those resources leak.

Recipe: implement CancelAction(). Override it on your executor:

void UMyExecutor::CancelAction()
{
    if (ClaimedSmartObject.IsValid())
    {
        SmartObjectSubsystem->Release(ClaimedSmartObject);
        ClaimedSmartObject.Invalidate();
    }
    if (ActiveTimelineHandle.IsValid())
    {
        ActiveTimelineHandle.Stop();
    }
}

The dispatcher task calls CancelAction() exactly once when the plan goes stale or the state is exited. There is no ExitAction follow-up on cancellation.


5. Squad assault: one agent provides cover while another flanks

Problem: needs coordination, but Intent Forge is per-agent.

Recipe: model the squad as shared world state.

  1. Create a custom USquadCoordinator actor that exposes IsLayingCoverFire(), IsFlankingNorth(), etc.
  2. In each agent's IntentForge sensor, copy the coordinator's flags into that agent's FWorldState (e.g., AllyCovering = true).
  3. The "flanking" agent's Action_Flank has precondition AllyCovering = true.
  4. The "cover" agent's Action_LayCoverFire has effect AllyCovering = true.

This is cooperative-by-convention rather than enforced. Full multi-agent planning is on the roadmap.


6. Designer-authored cost curve

Problem: cost should scale non-linearly. Linear FactWeighted isn't enough.

Recipe: subclass UIntentCostStrategy in Blueprint.

  1. Create BP_CostStrategy_Curved deriving from UIntentCostStrategy.
  2. Override Evaluate. In the graph, use a Curve Float asset, sample it with Get Scalar from the world state, and return the result.
  3. Set this strategy on whichever actions need curved costs.

Blueprint-authored cost strategies are ~5–20× slower than C++. For actions called many times per plan that's measurable; for archetypes with 20-ish actions it's still well within the per-plan budget.


7. Sense the world via sensors on the archetype

Problem: facts must reflect the live game world (health, target distance, ability cooldowns). Without sensors, every game system would need to call SetFactBool directly.

Recipe: declare sensors on the archetype. The component duplicates them per-agent on InitializeComponent, ticks them at their declared SampleInterval, and tears them down on archetype change or end-play.

  1. In your archetype asset's Sensor Defaults, add an instance of Sensor: Gameplay Tag On Actor. Set its TagToObserve (e.g. Combat.IsArmed) and ResultFactId (IsArmed).
  2. Add an instance of Sensor: Distance To Referenced Actor. Set TargetActorObjectFactId = "PrimaryEnemy" and ResultScalarFactId = "DistanceToEnemy".
  3. Somewhere in gameplay code (e.g. a perception adapter), populate the PrimaryEnemy object fact with an FIntentActorFact whose Actor points at the perceived target. The distance sensor reads it on its next tick and writes the scalar fact.

Custom sensors: subclass UIntentSensorBase (C++) or UIntentSensor_BP (Blueprint). Override Initialize to bind delegates (e.g. AIPerception), Sample to write facts, Deinitialize to release. Set SampleInterval for polling sensors; for event-driven sensors override Sample to a no-op and do the work in the bound delegate.

Event-driven without a sensor: for one-off events (e.g. taking damage), call IntentForgeComponent->SetFactBool directly from your gameplay code. The sensor framework exists for continuous observation, not every world-state mutation.


8. Stop planning when no target is visible

Problem: combat archetype shouldn't even consider attack goals if there's no target.

Recipe: gate the goal with a precondition.

  1. Add fact HasTarget (bool, replan-triggering).
  2. In each combat goal's DesiredState, the first row is Bool Fact EqualsHasTarget = true. (This makes the goal trivially unsatisfied when no target exists.)
  3. Since the goal can't be satisfied without a target, the planner returns no plan, and the component idles.

Alternative: write a UIntentGoalConsiderationSet Blueprint that returns 0 when HasTarget is false. The goal scores at 0 and gets skipped.


9. Inspect why an action wasn't chosen

Problem: the agent picked the wrong action; you want to understand why.

Recipe:

  1. Enable verbose planner logs:
    Log LogIntentForgePlanner VeryVerbose
  2. In PIE, with Gameplay Debugger open, find the IntentForge category. It shows the active plan and goal.
  3. For a single-frame snapshot, run the IntentForge.DumpAllAgents console command — it prints one line per agent with the goal, plan length, cost, and world version.

A richer "why didn't this action get chosen?" inspector is planned.


10. Hot-swap the archetype at runtime

Problem: a peasant becomes a soldier when their barracks is built.

Recipe:

  1. Get the IntentForge component.
  2. Call Set Archetype with the new archetype asset.

This will:

  • Cancel any in-flight action.
  • Reset the world state.
  • Rebind the schema.
  • Force a replan on the next tick.

Carry-over facts (e.g., the agent's name, current location) belong on the actor, not in FWorldState — the world state is intentionally wiped on archetype change.


11. Wire sight/hearing/damage perception in two minutes

Problem: an enemy needs to plan based on what it can see. Writing the AIPerception → fact glue from scratch is tedious.

Recipe: enable the IntentForgePerception module, then drop the sensor on your archetype.

  1. Your pawn (or its AIController) must already have a UAIPerceptionComponent configured with whichever senses you want (Sight, Hearing, etc.).
  2. In your archetype's Sensor Defaults, add an instance of Sensor: AI Perception.
  3. Set HasTargetFactId to a bool fact in your schema (e.g. HasTarget). Set TargetActorFactId to an object fact (e.g. Target). Leave SenseFilter null to react to any sense, or set it to UAISense_Sight to listen only to vision.
  4. Now your archetype's goals can use HasTarget = true as a precondition, and actions can pair with the distance sensor to compute range-to-target.

When a target is perceived, HasTarget flips to true and the object fact is populated with an FIntentActorFact. When the same target is lost (sense-loss event), both clear.

For multi-target priority schemes (e.g. "focus the closest visible enemy"), subclass UIntentSensor_AIPerception and override HandlePerceptionUpdated — the rest of the lifecycle is reused.


12. Time-travel debug a misbehaving agent

Problem: an agent picks the wrong action and you want to see the exact sequence of events leading up to it.

Recipe: use the Visual Logger.

  1. In PIE, press Ctrl+L (default binding) to open the Visual Logger.
  2. Select your pawn.
  3. Scrub the timeline. You'll see entries each time the planner adopts a new plan (goal, step count, cost) and each time an action completes (handle + terminal status).
  4. Pair with the Gameplay Debugger overlay (apostrophe key) for live world-state inspection at the same timestamp.

VL entries are stripped in Shipping builds — zero runtime cost in release.


13. Reserve a Smart Object slot with claim-and-hold

Problem: multiple agents want to use the same bench / workstation / cover point. Without coordination they'll fight over it.

Recipe: enable the IntentForgeSmartObjects module, then use SO: Claim And Hold as the executor for the "use this kind of thing" action.

  1. Make sure your SO actors have USmartObjectComponent configured with a USmartObjectDefinition (engine's standard SO authoring).
  2. Author an IntentForge action DA_Action_UseBench with executor SO: Claim And Hold. Set:
    • Activity Requirements: gameplay-tag query the candidate SO must satisfy (e.g. SmartObject.Activity.Rest).
    • Search Radius: e.g. 1000 cm.
    • Hold Seconds: how long the agent occupies the slot.
  3. Prepend a MoveTo action whose target is the slot's location. The claim executor does not walk the agent there — keeping the responsibilities separate means an agent can claim a slot before moving (a common reservation pattern).

The executor releases the slot on success and on cancellation, so plan-invalidation mid-hold cleanly returns the slot to the pool.


14. Live-inspect agents at-a-glance during PIE

Problem: Gameplay Debugger overlays a single selected agent, but you want to see every agent in the world at once and click between them.

Recipe: use the IntentForge Live Inspector tab.

  1. Window → Developer Tools → IntentForge Live Inspector. Dock it wherever you like (a vertical panel on the right works well).
  2. Start PIE. The left pane fills with every live agent (actor name | current goal | step count). Refresh is 0.5 s.
  3. Click an agent. The right pane shows:
    • Header: actor, archetype, world-state version
    • Current plan with an arrow on the current step
    • Every schema fact and its live value
    • Recent replan history (newest 8)
  4. Pair with the Visual Logger for after-the-fact "why did it do that?" replay.

The inspector uses UIntentForgeWorldSubsystem::GetAllAgents() — same registry exposed for your own debug tooling if you want a custom panel.


15. Find the slow sensor

Problem: your AI feels sluggish. You suspect a sensor is doing too much work per Sample. Profile it.

Recipe:

  1. In PIE, open the editor console.
  2. Run IntentForge.ProfileSensors <PartialActorName> (omit the arg to see every agent).
  3. The output lists each sensor's class, total sample count, accumulated time, and average ms per sample. The hot sensor is the one with high avg-ms or runaway sample count.

Common causes of slow sensors:

  • Sample interval too low (samples 60×/sec when 10× would do).
  • Expensive per-sample work (line traces, EQS queries) without caching.
  • Calling FindComponentByClass every sample (use UIntentForgeStatics::GetIntentForgeComponent and cache).

For Blueprint-authored sensors, expect 5–20× slower than C++ per sample. Rewrite to C++ if profiling shows it matters for your scale.


16. Surface authoring bugs with Analyze Archetype

Problem: your agent does nothing at runtime. Validator passed. Test Plan returns empty. What's wrong?

Recipe:

  1. Open your archetype asset.
  2. Under IntentForge Authoring in the details panel, click Analyze Archetype.
  3. The IntentForge MessageLog shows:
    • Orphan facts: facts referenced by preconditions / goal desired-state that NO action produces. These guarantee unreachable goals.
    • Unused actions: actions whose effects no consumer reads — usually a sign of a missing precondition or a typo'd fact id.

A clean run prints "OK: every consumed fact has at least one producer" and "OK: every action's effects are referenced." If you see orphans, trace the fact name and either spell it correctly or add a producing action.


17. Author a custom precondition type

Problem: the bundled BoolFactEquals and ScalarComparison preconditions don't cover your need (e.g. "fact within a numeric range", "tag query against actor's GAS"). Add a custom one.

Recipe (C++):

  1. Subclass FIntentPrecondition:

    USTRUCT(BlueprintType, DisplayName = "Scalar In Range")
    struct FPrecondition_ScalarInRange : public FIntentPrecondition
    {
        GENERATED_BODY()
        UPROPERTY(EditAnywhere) FName FactId;
        UPROPERTY(EditAnywhere) float Min = 0.f;
        UPROPERTY(EditAnywhere) float Max = 1.f;
        bool EvaluateAgainst(const FWorldState& State) const
        {
            const float V = State.GetScalar(FactId);
            return V >= Min && V <= Max;
        }
    };
  2. Register a dispatch handler at module startup so IntentForge::CheckPrecondition routes calls to your type:

    void FMyModule::StartupModule()
    {
        IntentForge::FPreconditionRegistry::Register(
            FPrecondition_ScalarInRange::StaticStruct(),
            [](const FInstancedStruct& S, const FWorldState& State) {
                return S.Get<FPrecondition_ScalarInRange>().EvaluateAgainst(State);
            });
    }
  3. (Optional) Teach the validator about your type by extending its GetReferencedFactId switch — otherwise typo'd fact ids won't be caught at save time for your custom shape.

Effect-shaped types follow the same pattern via FIntentEffect and the effect registry.


18. Stop a goal flapping near a threshold (Schmitt latch on a scalar fact)

Problem: you want a goal to fire when Health < 0.30, but the player takes a series of small hits and Health oscillates around the threshold (0.29, 0.31, 0.29, 0.32...). Without protection the goal flaps in/out every replan and the agent visibly stutters between Flee and Patrol.

Recipe (no C++):

  1. Declare Health as a Scalar fact and IsLowHealth as a Bool fact in your UFactSchemaAsset. Both with bTriggersReplan = true.
  2. On the same schema asset, populate the LatchedBoolFacts array with one row:
    • Source Scalar Fact Id: Health
    • Output Bool Fact Id: IsLowHealth
    • Enter Threshold: 0.30
    • Exit Threshold: 0.40
    • Initial Value: false
  3. Author your Flee goal's DesiredState to read IsLowHealth == false (so the agent flees while the latch is true).
  4. Sensors / damage events write Health via SetFactScalar; the schema derives IsLowHealth reactively. Health jittering between 0.29 and 0.31 no longer flips IsLowHealth — only a sustained drop below 0.30 trips the latch true, and only a sustained recovery above 0.40 trips it back.

Why this works: IsLowHealth carries memory (the latched state) that the raw Health scalar can't. Preconditions stay pure functions of world state; the hysteresis lives at the fact-derivation layer. See the Anti-Flap Toolkit for the full layered toolkit.


19. Smooth a noisy scalar fact (EMA filter)

Problem: a sensor writes DistanceToTarget every frame, but the target's position is jittery (e.g. animated rig) or the sensor itself has fast-vs-slow sampling artifacts. Goal scores tied to DistanceToTarget flap.

Recipe (no C++):

  1. On the relevant FFactSchemaEntry for DistanceToTarget (Scalar type), expand the Filter substruct in the details panel.
  2. Set bEnabled = true, Alpha = 0.25 (heavier smoothing for noisier inputs — try 0.10 for very noisy signals, 0.50 for mostly-clean ones).
  3. That's it. Every write to DistanceToTarget via SetFactScalar (or any sensor's OutState.SetScalar) is now Alpha*raw + (1-Alpha)*prior before storage. The Live Inspector's "Derived Facts" panel shows the raw input alongside the smoothed value so you can verify the curve.

Note on first-sample passthrough: the very first write of a filtered fact stores raw (no smoothing bias toward zero). The EMA only kicks in on the second and subsequent samples.

Note on planner search: the planner suppresses filtering during A* expansion (effects in simulated futures are deterministic, not noisy). Latching is not suppressed — derived bools are part of the symbolic state the planner reasons about. See the Anti-Flap Toolkit for why this asymmetry is the load-bearing rule.


20. Author a custom sensor from scratch

Problem: the bundled sensors (distance, line-of-sight, gameplay tag, AI perception, blackboard bridges) don't cover what you need — e.g. "ammo in the equipped weapon", "team-relative danger", "footstep noise recently heard". Write a custom one.

Recipe (C++):

  1. Subclass UIntentSensorBase:

    UCLASS(Blueprintable, meta = (DisplayName = "Sensor: Equipped Ammo"))
    class MYGAME_API UIntentSensor_EquippedAmmo : public UIntentSensorBase
    {
        GENERATED_BODY()
    public:
        /** Scalar fact (0..1) — fraction of magazine remaining. */
        UPROPERTY(EditAnywhere, Category = "Sensor",
            meta = (ToolTip = "Scalar fact that receives the ammo fraction."))
        FName ResultScalarFactId;
    
        virtual void Initialize(AActor* Owner) override;
        virtual void Sample(AActor* Owner, FWorldState& OutState) override;
        virtual void Deinitialize(AActor* Owner) override;
    };
  2. Implement the lifecycle:

    void UIntentSensor_EquippedAmmo::Initialize(AActor* Owner)
    {
        // Cache anything stable for the actor's lifetime.
        CachedWeaponComp = Owner->FindComponentByClass<UWeaponComponent>();
        SampleInterval = 0.2f;  // 5 Hz — ammo changes are slow
    }
    
    void UIntentSensor_EquippedAmmo::Sample(AActor* Owner, FWorldState& OutState)
    {
        if (!CachedWeaponComp.IsValid() || ResultScalarFactId.IsNone()) return;
        const float Fraction = CachedWeaponComp->GetAmmoFraction();
        OutState.SetScalar(ResultScalarFactId, Fraction);
    }
    
    void UIntentSensor_EquippedAmmo::Deinitialize(AActor* Owner)
    {
        CachedWeaponComp = nullptr;
    }
  3. In your archetype's Sensor Defaults, add an instance of Sensor: Equipped Ammo. Set ResultScalarFactId = "AmmoFraction". The schema's AmmoFraction fact must exist and be Scalar.

Why this pattern: SampleInterval is honored by the component's timer scheduler — your Sample runs at exactly the rate you declare, not every tick. The component duplicates the sensor template per-agent on InitializeComponent, so CachedWeaponComp is per-agent state.

Event-driven instead of polling: for "fact-on-event" patterns (ammo fired, hit landed), don't poll — bind a delegate in Initialize, write the fact from the delegate handler, and set SampleInterval to a large number (the framework still calls Sample as a fallback). Or override Sample to a no-op. The fact write triggers replan-coalesce regardless of which code path wrote it.

Blueprint variant: subclass UIntentSensor_BP instead. The lifecycle hooks are exposed as Blueprint events with the same signatures. Expect 5–20× slower than C++ per Sample; profile with IntentForge.ProfileSensors if it matters at your scale (see recipe 15).


21. Author a custom action executor

Problem: the bundled executors (MoveTo, Wait, PlayAnim, RunBlueprintFunction, RotateToFaceLocation, SO:ClaimAndHold, etc.) don't cover your action. The action's job is "do this specific thing in the world" — write an executor for it.

Recipe (C++):

  1. Subclass UIntentActionExecutor:

    UCLASS(Blueprintable, meta = (DisplayName = "Executor: Throw Grenade"))
    class MYGAME_API UExecutor_ThrowGrenade : public UIntentActionExecutor
    {
        GENERATED_BODY()
    public:
        UPROPERTY(EditAnywhere, Category = "Throw")
        TSubclassOf<AActor> GrenadeClass;
    
        virtual void EnterAction(AActor* OwnerActor,
            const FInstancedStruct& Params) override;
        virtual EActionStatus TickAction(float DeltaTime) override;
        virtual void ExitAction(EActionStatus FinalStatus) override;
        virtual void CancelAction() override;
    
    private:
        float ChargeTime = 0.f;
        bool bThrown = false;
        TWeakObjectPtr<AActor> SpawnedGrenade;
    };
  2. Implement the lifecycle. The contract is exactly four methods:

    void UExecutor_ThrowGrenade::EnterAction(AActor* OwnerActor,
        const FInstancedStruct& Params)
    {
        ChargeTime = 0.f;
        bThrown = false;
        // Play wind-up anim, lock locomotion, etc.
    }
    
    EActionStatus UExecutor_ThrowGrenade::TickAction(float DeltaTime)
    {
        ChargeTime += DeltaTime;
        if (!bThrown && ChargeTime >= 0.6f)
        {
            SpawnedGrenade = GetWorld()->SpawnActor<AActor>(GrenadeClass,
                GetOwner()->GetActorLocation(),
                GetOwner()->GetActorRotation());
            bThrown = true;
        }
        if (ChargeTime >= 1.2f)  // total: 0.6s wind-up + 0.6s recovery
        {
            return EActionStatus::Succeeded;
        }
        return EActionStatus::Running;
    }
    
    void UExecutor_ThrowGrenade::ExitAction(EActionStatus FinalStatus)
    {
        // Restore locomotion lock, stop anim, etc.
    }
    
    void UExecutor_ThrowGrenade::CancelAction()
    {
        // The plan went stale mid-throw. Decide: keep the spawned grenade
        // (it's already a live actor) but unwind the executor's own state.
        // CancelAction is called instead of ExitAction — there is no
        // follow-up ExitAction after a cancel.
    }
  3. Set ExecutorClass = UExecutor_ThrowGrenade::StaticClass() on the DA_Action_ThrowGrenade action. (Default Params on the action is an FInstancedStruct — populate with whatever your executor reads from the Params arg to EnterAction.)

The four methods, in plain language:

MethodWhenJob
EnterActionOnce, on dispatchRead params, allocate resources, kick off any background work
TickActionEvery dispatcher tickDrive state forward; return Running / Succeeded / Failed
ExitActionOnce, when TickAction returned terminalRelease resources; cleanup is the only allowed work
CancelActionOnce, when the plan went stale mid-stepRelease resources; no ExitAction follow-up

Blueprint variant: subclass UIntentActionExecutor_BP. The four methods become Blueprint events. The dispatcher tick interval is whatever the dispatcher uses — UIntentForgeDispatcherSubsystem ticks once per frame; the StateTree task ticks at the StateTree's rate.

See also: recipe 4 (releasing external resources via CancelAction).


22. Debug a stuck or misbehaving agent

Problem: the agent isn't doing what you expect. It might be standing still, looping the same action, picking the wrong goal, or just not spawning a plan. Walk through the diagnostic ladder.

Recipe: in this order, stop when one of the steps reveals the cause.

  1. Is the agent registered?

    IntentForge.AgentCount

    Zero means the component isn't on the actor, or the component's Archetype is null. Fix the spawn / Details panel and retry.

  2. What does the planner think the agent should be doing?

    IntentForge.DumpAgent <PartialActorName>

    Output includes the active goal, plan steps, current step index, and the world version. If the goal is empty: no goal scored above its considerations cutoff. If the plan is empty: no satisfiable goal. If the step index is stuck: something is preventing TickAction from returning terminal.

  3. Why no plan?

    • Open the archetype asset.
    • Click Analyze Archetype (see recipe 16). Orphan facts surfaced here are the single most common cause of "no plan" — a goal needs HasTarget = true but no action produces it, and no sensor writes it.
  4. Live inspect the world state.

    • Window → Developer Tools → IntentForge Live Inspector (recipe 14).
    • Click the agent. Cross-check every fact value against what a working plan would need. If HasTarget is false when you expected true, trace back to the sensor (recipe 7) or the gameplay code that should have written it.
  5. What did the planner consider?

    • Open Visual Logger (recipe 12, default Ctrl+L).
    • Select the agent, scrub to a recent replan. Each entry lists every scored goal with its score and priority. The goal you expected is either: not present (precondition failed — check DesiredState of the goal vs current facts), scoring 0 (consideration set returned 0 — check the consideration curve), or scoring below another goal (a different goal is winning — check why).
  6. Is the action stuck?

    • If DumpAgent shows the same plan step for many seconds, the executor's TickAction is returning Running forever. Check the executor — likely missing a terminal condition or waiting on something that never happens. MinActionCommitTime will also keep the agent on an action for at least that long after starting it; check the component's MinActionCommitTime setting if you expected faster preemption.
  7. Is a slow sensor starving the planner?

    IntentForge.ProfileSensors <PartialActorName>

    Sensors that take ms-per-Sample at high frequencies will starve the game thread before the planner does. Recipe 15 covers the fix paths.

  8. Last resort: verbose planner logs.

    Log LogIntentForgePlanner VeryVerbose

    Logs every goal scoring decision and every A* node expansion. Drastically slows planning — only use when nothing else points at the problem.

Common patterns and their fix:

SymptomLikely causeFix
AgentCount = 0Archetype not assigned, or actor doesn't have the componentDrop component in BP / set archetype in Details
Plan adopted, then immediately replaced every tickFlapping between two equal-score goalsSet GoalMomentumBonus > 0; add a Schmitt latch on the driving scalar fact (recipe 18)
Plan empty, "no satisfiable goal"Goal needs a fact no action producesAnalyze Archetype → orphan fact → add producer
Plan starts but never advancesExecutor returns Running foreverInspect the executor's terminal logic; missing condition
Plan adopts a wrong goalConsideration curves return wrong scoresVisual Logger entry shows scored goals; check the curve
Agent picks correctly in editor but not at runtimebTriggersReplan missing on a key fact, or sensor not wiredSchema: enable replan-triggering on the fact; verify sensor in archetype

If you've worked through this list and still can't find it, file an issue on the plugin repo with the IntentForge.DumpAgent output and the relevant Visual Logger screenshot — that's enough information to reproduce most issues.


23. Dispatch from a Behavior Tree

Problem: your project already drives AI from Behavior Trees and you want Intent Forge to be a task in the tree rather than replace the tree. The planner picks the action; the BT decides when to run the planner.

Recipe (no C++):

  1. Make sure the IntentForgeBT module is enabled. (It's an optional module that ships with the plugin; enable it under Edit → Plugins → search "Intent Forge BT" if it isn't on already.)

  2. On your pawn, add a UIntentForgeComponent (same as the Quickstart). Set its Archetype to the archetype you want the planner to drive. Leave Dispatch Mode at Auto — the BT task will switch it to BehaviorTree when active and restore it when finished.

  3. Open the AIController's Behavior Tree asset. Right-click in the graph → Add Task → search for Run Intent Forge Action (the task's UCLASS DisplayName).

  4. Wire the task into the tree wherever you want planner-driven behaviour. The task drives one action at a time: it spawns the executor, ticks it, calls NotifyActionCompleted on terminal status, then spawns the next step. It only returns from the task when the plan is empty or the BT aborts it.

  5. Compile and Save the BT.

Why this works: UBTTask_RunIntentForgeAction resolves the component on the controlled pawn (or the controller's actor), claims ownership by setting DispatchMode = BehaviorTree for the duration, and runs the same executor lifecycle (EnterAction / TickAction / ExitAction / CancelAction) the Auto dispatcher runs. The component side of the contract is identical regardless of dispatcher — the planner decides; the BT task is just the actor that calls the lifecycle methods.

When to use this dispatcher:

  • You have legacy BTs you want to keep.
  • Your BT mixes planner-driven action selection (the Intent Forge task) with hand-authored decision selectors (move-to-cover, wait for cue, etc.).

When NOT to use it: if your AI is entirely planner-driven, skip the BT and use DispatchMode = Auto. The BT adds a layer with no benefit.


24. Dispatch from a StateTree

Problem: same as recipe 23, but for StateTree instead of BT.

Recipe (no C++):

  1. Enable the IntentForgeStateTree module under Edit → Plugins.

  2. On the executing actor, add a UIntentForgeComponent and set its Archetype. Leave Dispatch Mode at Auto.

  3. Open the StateTree asset. The task is FIntentForgeRunActionTask with UCLASS DisplayName "Intent Forge: Run Action". Add it to a state in your tree:

    • Click the state.
    • In the right-hand details panel, find the Tasks array.
    • Click + → search for Intent Forge: Run Action and select it.
  4. The task takes the UIntentForgeComponent as a TStateTreeExternalDataHandle<UIntentForgeComponent>. Bind it in the state-tree schema (the actor schema usually auto-discovers it from the owning actor's component).

  5. Save the StateTree.

Why this works: same contract as the BT task. The StateTree task claims DispatchMode = StateTree while it's running, drives the executor lifecycle, and restores the previous DispatchMode on exit. Plan invalidation is detected by comparing LastObservedPlanGeneration against Component.GetPlanGeneration() — if the component replans and the new first-step doesn't match the running executor, the task tears down + respawns.

When to use it: same logic as recipe 23. Pick the runtime that matches your project's existing convention.


25. External dispatch — drive the lifecycle from your own code

Problem: you have a custom AI orchestrator (your own state machine, a third-party plugin, custom Blueprint AI) and you want Intent Forge to plan but your code to drive the action lifecycle. Neither Auto nor a BT/StateTree task is the right fit.

Recipe (C++):

  1. On the component, set DispatchMode = EIntentDispatchMode::External. This makes UIntentForgeDispatcherSubsystem skip the component entirely — you take full responsibility for spawning executors and reporting terminal status.

  2. In your tick loop (or whenever your orchestrator decides to step the action lifecycle), implement this loop per agent:

    void FMyOrchestrator::TickAgent(UIntentForgeComponent& Component,
                                    FMyAgentState& State,
                                    float DeltaTime)
    {
        // 1. Detect plan invalidation. If the component replanned and
        //    the active action changed, tear down + respawn.
        const int32 CurrentGen = Component.GetPlanGeneration();
        if (CurrentGen != State.LastObservedPlanGeneration)
        {
            if (State.CurrentExecutor)
            {
                State.CurrentExecutor->CancelAction();
                State.CurrentExecutor = nullptr;
            }
            State.LastObservedPlanGeneration = CurrentGen;
        }
    
        // 2. If no executor is running, try to spawn one for the
        //    component's current step.
        if (!State.CurrentExecutor)
        {
            const FIntentAction* Action = Component.GetCurrentAction();
            if (!Action || !Action->ExecutorClass) return;
    
            State.CurrentExecutor = NewObject<UIntentActionExecutor>(
                &Component, Action->ExecutorClass);
            State.CurrentExecutor->EnterAction(
                Component.GetOwner(), Action->DefaultParams);
        }
    
        // 3. Tick the executor. On terminal status, exit + notify the
        //    component, then let the next iteration spawn the next step.
        const EIntentActionStatus Status =
            State.CurrentExecutor->TickAction(DeltaTime);
        if (Status != EIntentActionStatus::Running)
        {
            State.CurrentExecutor->ExitAction(Status);
            State.CurrentExecutor = nullptr;
            Component.NotifyActionCompleted(Status);
        }
    }
  3. The FMyAgentState struct only needs two fields:

    struct FMyAgentState
    {
        TObjectPtr<UIntentActionExecutor> CurrentExecutor = nullptr;
        int32 LastObservedPlanGeneration = 0;
    };

    Mark CurrentExecutor with UPROPERTY() if you're storing the state on a UObject — otherwise the GC will reap your executor mid-action.

Why this works: the External dispatcher mode tells the built-in dispatcher subsystem to leave the component alone. Your code is now playing the same role the subsystem plays for Auto components: spawn executors, tick them, detect plan invalidation via PlanGeneration, report terminals. The component-side contract is identical.

When to use it:

  • You're integrating Intent Forge into an existing custom AI runtime.
  • You need finer control over when the executor ticks (e.g. tier-based ticking — front-of-camera agents tick every frame, distant agents tick every fourth frame).
  • You're prototyping a new dispatch strategy that doesn't fit Auto/BT/StateTree.

Gotchas:

  • Forgetting to NotifyActionCompleted leaves the component waiting forever — it has no other signal that the action terminated.
  • Calling EnterAction twice for the same handle is undefined; always null out CurrentExecutor after ExitAction.
  • The component owns the canonical strong reference to the executor via its ActiveExecutor UPROPERTY; treat your state struct's reference as observational only.

On this page