Skip to main content
Flow Core Documentation
Menu

Extension

Authoring Custom Instructions

Add a supported typed operation that designers can run inside an Action.

Audience
Unity developers
Time
25 minutes
Requires
C#, Flow Core runtime assembly access, and codegen access
Modules
Flow Core, C#
On this page

Create a custom Instruction when Flow Core should perform a project-specific operation inside an Action list. Keep the runtime contract typed, explicit, and free of editor dependencies.

Define runtime data and behavior

The Instruction below resolves a typed string and logs it:

using System;
using System.Threading.Tasks;
using UnityEngine;
using TwoCatsCode.FlowCore;

[FlowInstruction("Log Project Message", "Project", "Log a typed string value.")]
public sealed class InstructionLogProjectMessage : IInstruction
{
    [Serializable]
    public sealed class Data : InstructionData
    {
        [Tooltip("Provides the string written to the Console when this Instruction executes. Select a literal or a typed context value; failed resolution does not write a message.")]
        public FlowValueGetStringData Message = new();

        public override InstructionData Clone()
        {
            return new Data
            {
                Message = Message?.Clone() as FlowValueGetStringData
                    ?? new FlowValueGetStringData()
            };
        }

        public override string Summary(FlowContext context)
        {
            return $"Message: {Message?.Summary(context)}";
        }
    }

    private readonly Data data;

    public InstructionLogProjectMessage(Data data)
    {
        this.data = data ?? new Data();
        this.data.Message ??= new FlowValueGetStringData();
    }

    public ValueTask<InstructionResult> ExecuteAsync(FlowContext context)
    {
        if (FlowValueResolveHelper.TryResolveValue(
                data.Message, context, out string value, out _))
        {
            Debug.Log(value);
        }

        return new ValueTask<InstructionResult>(InstructionResult.Continue);
    }

    public string Summary(FlowContext context) => string.Empty;
}

Preserve the contract

  • Put the type in a runtime assembly.
  • Keep serialized fields inside Data.
  • Deep-clone nested Flow value data.
  • Resolve typed values through the supplied FlowContext.
  • Return an intentional InstructionResult.
  • Keep editor UI code out of runtime assemblies.
  • Avoid allocations and reflection in hot paths.

Control optional fields

Use FlowFieldAutoUI for simple authoring metadata. For example, optional prefix fields inside an Instruction’s Data can be declared as:

[Tooltip("Enables an optional prefix for the custom logging operation. Apply this choice explicitly when resolving the message at runtime.")]
public bool UsePrefix;

[Tooltip("Text to prepend when UsePrefix is enabled. Hiding this field preserves its value; clone it with the other Instruction data.")]
[FlowFieldAutoUI(Label = "Prefix", Show = "$UsePrefix")]
public string Prefix = string.Empty;

With reflection UI, changing UsePrefix immediately updates the Prefix field’s visibility. Hiding it preserves its value. The attribute controls drawing only: if you add these fields to the logging example, clone them in Clone and explicitly apply UsePrefix in the runtime operation.

The reflection fallback supports boolean members, nested member paths, an optional $ prefix, !, boolean and enum comparisons using == and !=, &&, ||, and parentheses. A nullable boolean (bool?) with a null value evaluates as false. It is not an arbitrary C# expression evaluator: method calls, arithmetic, and relational comparisons such as >= are unsupported. Keep property getters inexpensive and free of side effects.

An invalid condition leaves that field visible and reports a Console warning at most once per type, field, and condition combination per script load. Other valid Source, Target, and container rules still apply. Visibility is an authoring aid, not runtime validation.

Register and verify

  1. Import the supported runtime source and any required helpers. Flow Core 6.0 supplies its binding during compilation; no central registry-generation step is required.
  2. Wait for Unity to compile without errors, then check Validate Available Bindings in Registry Generator if the entry is missing.
  3. Add an Action to a test graph.
  4. Search for Project > Log Project Message in the Instruction picker.
  5. Bind a literal and a Blackboard-backed string in separate tests.
  6. Enter Play Mode and confirm both resolve.

The package includes generated Drawers for its built-in types. Regenerate separately maintained Drawer implementations when their templates or field contracts change. Keep the existing output roots and review the generated diff; do not patch generated files by hand. In 6.0, compiler bindings replace the separate central registration-file maintenance. Handwritten custom Drawers keep responsibility for their own UI.

Reflection-only regions receive reflection behavior without regeneration. Use Generator only when you need implementation helpers; importing a supported source type is enough for automatic binding after compilation. Opening an editor or expanding an entry does not require Generator. See Automatic Registration & Prewarm for dependency, removal, and Player verification steps.

After registration, select the test Action in the Flow Inspector and expand Log Project Message. Use its More > Open Editor command to inspect the same data in a dedicated Instruction editor. Use Information to read the field metadata and Flow Knowledge to locate its reference. Keep the Tooltip useful to the author; reference metadata does not replace the runtime tests above.

Native picker, expanded Instruction and Console crops show Project/Log Project Message reading ProjectMessage from Local Blackboard 0 and logging Blackboard-backed project message.

The picker identifies the registered Project Instruction. The cropped Action details bind Message to Local[0].ProjectMessage, and the Console message below shows the resolved string from the real LogBlackboard execution.

Common Mistakes

  • Ignoring compiler/provider diagnostics or importing runtime code into an Editor-only assembly.
  • Returning shared nested Data from Clone.
  • Storing per-execution state on the shared graph asset or Instruction data.
  • Adding UnityEditor references to the runtime assembly.
  • Swallowing a failed value resolution while implying the operation succeeded.
  • Using reflection in a frequently executed Instruction.

What You Built

You added a typed runtime operation that designers can place inside any Action, configure with literals or Blackboard values, serialize with a graph, and execute without editor-only dependencies.