# Custom Instructions

An **Instruction** derives from `Instruction` and implements an operation in `Run`. Its attributes, serialized Signals, title, and execution contract establish the pattern shared by other node types.

## A minimal Instruction

This complete Instruction resolves a String Signal, logs the result, and finishes synchronously:

```cs
using System;
using UnityEngine;
using VisualScript.Icons;
using VisualScript.Runtime;

[Title("Log Text")]
[Description("Prints text in the Console")]
[Category("Debug/Log Text")]

[Parameter("Text", "The text to print")]
[Keywords("Print", "Console", "Debug")]

[Image(typeof(IconSpeech), ColorTheme.Type.TextLight)]

[Serializable]
public class InstructionMyLogText : Instruction
{
    [SerializeField] private GetString m_Text = new GetString();

    protected override string GetTitle(GameObject source) => $"Log {this.m_Text}";

    protected override Awaitable Run(Args args)
    {
        Debug.Log(this.m_Text.Get(args));
        return Done;
    }
}
```

`InstructionMyLogText` resolves `m_Text` only when `Run` executes and returns the shared `Done` awaitable. Its parts separate editor configuration from runtime behavior:

| Part | Controls |
| :--- | :--- |
| Attributes | Search, documentation, category, and appearance. |
| Serialized fields | Designer configuration stored with the Event. |
| `Run` | Runtime behavior and completion. |

Return `Done` when the work finishes synchronously. It's a shared, pre-completed awaitable, so returning it allocates nothing.

## Taking Signals, not values

The field is `GetString`, not `string`. Signal wrappers preserve designer choice at runtime.

A `string` field accepts one literal. A `GetString` field accepts any String Signal, including a variable, game object's name, or concatenation.

The same pattern applies to other value types. This partial example resolves a game object and an amount; the operation using them is omitted:

```cs
[SerializeField] private GetGameObject m_Target = new GetGameObject();
[SerializeField] private GetNumber m_Amount = new GetNumber();

protected override Awaitable Run(Args args)
{
    GameObject target = this.m_Target.Get(args);
    float amount = this.m_Amount.GetFloat(args);
    // …
}
```

Resolve Signals inside `Run` with the supplied `args`. The Instruction instance is shared, while each result depends on context, so never cache the resolved value in a field.

## Instructions that take time

`Run` returns an `Awaitable`, so an Instruction can suspend the containing list:

```cs
protected override async Awaitable Run(Args args)
{
    Debug.Log("Starting");
    await this.WaitForSeconds(2f);
    Debug.Log("Finished");
}
```

The list waits between the two log calls and resumes after `WaitForSeconds` completes.

Four helpers are available, and all of them respect cancellation:

| Helper | Suspends until |
| :--- | :--- |
| `NextFrame()` | The next frame. |
| `NextFixedFrame()` | The next physics step. |
| `WaitForSeconds(duration, timeMode)` | The duration elapses. |
| `While(func)` / `Until(func)` | The predicate flips. |

!!! warning "Raw waits don't check Event cancellation"
    A plain `await Awaitable.WaitForSecondsAsync(2f)` doesn't check whether the Event was canceled. After the wait, an Instruction can continue and access an object that's been destroyed.

    The helpers check `ParentCancelToken` every frame and return when cancellation is requested. Check the same token in a custom loop:

    ```cs
    while (!this.ParentCancelToken.Get)
    {
        await this.NextFrame();
    }
    ```

    `NextFrame` preserves the cancellation check while yielding control to Unity.

## Reporting a result

By default an Instruction reports success. Set `CurrentResult` to change what happens next:

| Result | Effect |
| :--- | :--- |
| `InstructionResult.Success` | Continue. The default. |
| `InstructionResult.Failure` | Continue, but mark this as not having run its body. |
| `InstructionResult.Restart` | Jump back to the top of the list. |
| `InstructionResult.Exit` | Stop the list. |

`Failure` lets **Else** respond when the preceding branch didn't run. It doesn't stop the list. This implementation reports the branch result explicitly:

```cs
protected override async Awaitable Run(Args args)
{
    if (this.m_Conditions.Check(args, Conditions.DEFAULT_OPERATION))
    {
        await this.m_Instructions.Run(args, this.ParentCancelToken);
        this.CurrentResult = InstructionResult.Success;
    }
    else
    {
        this.CurrentResult = InstructionResult.Failure;
    }
}
```

`PreviousResult` exposes what the preceding Instruction reported. **Else** and **Else If** use it to decide whether to run.

## Nested lists

An Instruction can hold its own `Conditions` and `Instructions`. These two attributes tell the editor to draw the nested lists; the remaining class members are omitted:

```cs
[NestInnerList(nameof(m_Conditions))]
[NestOuterList(nameof(m_Instructions))]

[Serializable]
public class InstructionMyIf : Instruction
{
    [SerializeField] private Conditions m_Conditions = new Conditions();
    [SerializeField] private Instructions m_Instructions = new Instructions();
    // …
}
```

Pass `this.ParentCancelToken` when running a nested list, so canceling the outer Event cancels the inner one too.

## Handling cancellation

Override `OnCancel` when there's cleanup to do, and propagate to any nested list:

```cs
public override void OnCancel()
{
    base.OnCancel();
    this.m_Instructions.Cancel();
}
```

`OnCancel` runs on the Instruction that was executing when the Event stopped. It doesn't undo operations that already completed.

## Exposing parameters to nested lists

If your Instruction makes extra values available to its nested list, implement `IParameterScope` so the editor offers them. See [Args](../args.md#declaring-a-parameter-scope).

## Checklist

- `[Serializable]`, derived from `Instruction`.
- Attributes filled in, especially `[Description]` and `[Parameter]`.
- Fields typed as Signals rather than raw values.
- `GetTitle` returns a readable phrase.
- Long-running work uses the cancellation-aware helpers.
- Nested lists receive `ParentCancelToken` and are canceled in `OnCancel`.

## Where to go next

- **[Custom Conditions](conditions.md)** — use the same shape for a synchronous boolean check.
- **[Custom Triggers](triggers.md)** — start an Event instead of running inside one.
- **[Args](../args.md)** — follow the context `Run` receives.
