# Jaction

> JAction - Chainable Task Execution

- Skill: `jasonxudeveloper/jaction` (Agent Skill)
- Install (CLI): `npx skillmds@latest add jasonxudeveloper/jaction`
- Raw SKILL.md: https://api.skillmd.com/api/skills/jasonxudeveloper/jaction/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Productivity
- Author: jasonxudeveloper (https://skillmd.com/u/jasonxudeveloper)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/jasonxudeveloper/jaction

---


# JAction - Chainable Task Execution

Fluent API for composing complex action sequences in Unity with automatic object pooling, zero-allocation async, and parallel execution support.

## When to Use

- Sequential workflows with delays
- Polling conditions (WaitUntil/WaitWhile)
- Repeat loops with intervals
- Game timers and scheduled events
- Zero-GC async operations
- Parallel concurrent executions

## Core Concepts

### Task Snapshot Isolation

When `Execute()` or `ExecuteAsync()` is called, the current task list is **snapshotted**. Modifications to the JAction after execution starts do NOT affect running executions:

```csharp
var action = JAction.Create()
    .Delay(1f)
    .Do(static () => Debug.Log("Original"));

var handle = action.ExecuteAsync();

// This task is NOT executed by the handle above - it was added after the snapshot
action.Do(static () => Debug.Log("Added Later"));

await handle; // Only prints "Original"
```

This isolation enables safe parallel execution where each handle operates on its own task snapshot.

### Return Types

**JActionExecution** (returned by Execute, awaited from ExecuteAsync):
- `.Action` - The JAction that was executed
- `.Cancelled` - Whether THIS specific execution was cancelled
- `.Executing` - Whether the action is still executing
- `.Dispose()` - Returns JAction to pool

**JActionExecutionHandle** (returned by ExecuteAsync before await):
- `.Action` - The JAction being executed
- `.Cancelled` - Whether this execution is cancelled
- `.Executing` - Whether still running
- `.Cancel()` - Cancel THIS specific execution
- `.AsUniTask()` - Convert to `UniTask<JActionExecution>`
- Awaitable: `await handle` returns `JActionExecution`

## API Reference

### Execution

| Method | Returns | Description |
|--------|---------|-------------|
| `.Execute(timeout)` | `JActionExecution` | Synchronous blocking execution |
| `.ExecuteAsync(timeout)` | `JActionExecutionHandle` | Async via PlayerLoop (recommended) |

### Actions

| Method | Description |
|--------|-------------|
| `.Do(Action)` | Execute synchronous action |
| `.Do<T>(Action<T>, T)` | Execute with state (zero-alloc for reference types) |
| `.Do(Func<JActionAwaitable>)` | Execute async action |
| `.Do<T>(Func<T, JActionAwaitable>, T)` | Async with state |

### Timing

| Method | Description |
|--------|-------------|
| `.Delay(seconds)` | Wait specified seconds |
| `.DelayFrame(frames)` | Wait specified frame count |
| `.WaitUntil(condition, frequency, timeout)` | Wait until condition true |
| `.WaitWhile(condition, frequency, timeout)` | Wait while condition true |

### Loops

| Method | Description |
|--------|-------------|
| `.Repeat(action, count, interval)` | Repeat N times |
| `.RepeatWhile(action, condition, frequency, timeout)` | Repeat while condition true |
| `.RepeatUntil(action, condition, frequency, timeout)` | Repeat until condition true |

All loop methods have `<TState>` overloads for zero-allocation with reference types.

### Configuration

| Method | Description |
|--------|-------------|
| `.Parallel()` | Enable concurrent execution mode |
| `.OnCancel(callback)` | Register cancellation callback |
| `.Cancel()` | Stop ALL active executions |
| `.Reset()` | Clear state for reuse |
| `.Dispose()` | Return to object pool |

### Static Members

| Member | Description |
|--------|-------------|
| `JAction.Create()` | Get pooled instance |
| `JAction.PooledCount` | Check available pooled instances |
| `JAction.ClearPool()` | Empty the pool |

## Patterns

### Basic Sequence

```csharp
using var result = await JAction.Create()
    .Do(static () => Debug.Log("Step 1"))
    .Delay(1f)
    .Do(static () => Debug.Log("Step 2"))
    .ExecuteAsync();
```

### Always Use `using var` (CRITICAL)

```csharp
// CORRECT - auto-disposes and returns to pool
using var result = await JAction.Create()
    .Do(() => LoadAsset())
    .WaitUntil(() => assetLoaded)
    .ExecuteAsync();

// WRONG - memory leak, never returns to pool
await JAction.Create()
    .Do(() => LoadAsset())
    .ExecuteAsync();
```

### Parallel Execution with Per-Execution Cancellation

```csharp
var action = JAction.Create()
    .Parallel()
    .Do(static () => Debug.Log("Start"))
    .Delay(5f)
    .Do(static () => Debug.Log("Done"));

// Start multiple concurrent executions (each gets own task snapshot)
var handle1 = action.ExecuteAsync();
var handle2 = action.ExecuteAsync();

// Cancel only the first execution
handle1.Cancel();

// Each has independent Cancelled state
var result1 = await handle1;  // result1.Cancelled == true
var result2 = await handle2;  // result2.Cancelled == false

action.Dispose();
```

### UniTask.WhenAll with Parallel

```csharp
var action = JAction.Create()
    .Parallel()
    .Delay(1f)
    .Do(static () => Debug.Log("Done"));

var handle1 = action.ExecuteAsync();
var handle2 = action.ExecuteAsync();

await UniTask.WhenAll(handle1.AsUniTask(), handle2.AsUniTask());

action.Dispose();
```

### Zero-Allocation with Reference Types

```csharp
// CORRECT - static lambda + reference type state = zero allocation
var data = new MyData();
JAction.Create()
    .Do(static (MyData d) => d.Process(), data)
    .Execute();

// Pass 'this' when inside a class - no wrapper needed
public class Enemy : MonoBehaviour
{
    public bool IsStunned;

    public void ApplyStun(float duration)
    {
        IsStunned = true;
        JAction.Create()
            .Delay(duration)
            .Do(static (Enemy self) => self.IsStunned = false, this)
            .ExecuteAsync().Forget();
    }
}

// Value types use closures (boxing would defeat zero-alloc anyway)
int count = 5;
JAction.Create()
    .Do(() => Debug.Log($"Count: {count}"))
    .Execute();
```

### Timeout Handling

```csharp
using var result = await JAction.Create()
    .WaitUntil(() => networkReady)
    .ExecuteAsync(timeout: 30f);

if (result.Cancelled)
    Debug.Log("Timed out!");
```

### Cancellation Callback

```csharp
var action = JAction.Create()
    .OnCancel(() => Debug.Log("Cancelled!"))
    .Delay(10f);

var handle = action.ExecuteAsync();
handle.Cancel();  // Triggers OnCancel callback
```

## Game Patterns

### Cooldown Timer

```csharp
public class AbilitySystem
{
    public bool CanUse = true;

    public async UniTaskVoid TryUseAbility(float cooldown)
    {
        if (!CanUse) return;
        CanUse = false;

        PerformAbility();

        // Pass 'this' as state - no extra class needed
        using var _ = await JAction.Create()
            .Delay(cooldown)
            .Do(static s => s.CanUse = true, this)
            .ExecuteAsync();
    }
}
```

### Damage Over Time

```csharp
public sealed class DoTState
{
    public IDamageable Target;
    public float DamagePerTick;
}

public static async UniTaskVoid ApplyDoT(
    IDamageable target, float damage, int ticks, float interval)
{
    var state = JObjectPool.Shared<DoTState>().Rent();
    state.Target = target;
    state.DamagePerTick = damage;

    using var _ = await JAction.Create()
        .Repeat(
            static s => s.Target?.TakeDamage(s.DamagePerTick),
            state, count: ticks, interval: interval)
        .ExecuteAsync();

    state.Target = null;
    JObjectPool.Shared<DoTState>().Return(state);
}
```

### Wave Spawner

```csharp
public async UniTask RunWaves(WaveConfig[] waves)
{
    foreach (var wave in waves)
    {
        using var result = await JAction.Create()
            .Do(() => UI.ShowWaveStart(wave.Number))
            .Delay(2f)
            .Do(() => SpawnWave(wave))
            .WaitUntil(() => ActiveEnemyCount == 0, timeout: 120f)
            .Delay(wave.DelayAfter)
            .ExecuteAsync();

        if (result.Cancelled) break;
    }
}
```

### Health Regeneration

```csharp
public sealed class RegenState
{
    public float Health, MaxHealth, HpPerTick;
}

public static async UniTaskVoid StartRegen(RegenState state)
{
    using var _ = await JAction.Create()
        .RepeatWhile(
            static s => s.Health = MathF.Min(s.Health + s.HpPerTick, s.MaxHealth),
            static s => s.Health < s.MaxHealth,
            state, frequency: 0.1f)
        .ExecuteAsync();
}
```

## Troubleshooting

| Problem | Cause | Solution |
|---------|-------|----------|
| Nothing happens | Forgot to call Execute/ExecuteAsync | Add `.ExecuteAsync()` at the end |
| Memory leak | Missing `using var` | Always use `using var result = await ...` |
| Frame drops | Using `Execute()` | Switch to `ExecuteAsync()` |
| GC allocations | Closures with reference types | Use static lambda + state parameter |
| Unexpected timing | Value type state | Wrap in reference type or use closure |
| Handle shows wrong Cancelled | Reading after modification | Snapshot is isolated - this is expected |

## Common Mistakes

1. **Missing `using var`** - Memory leak, JAction never returns to pool
2. **Using `Execute()` in production** - Blocks main thread, causes frame drops
3. **State overloads with value types** - Causes boxing; use closures instead
4. **Forgetting Execute/ExecuteAsync** - Nothing happens
5. **Heavy work in `.Do()`** - Callbacks run atomically; keep them lightweight
6. **Using `action.Cancel()` in parallel** - Cancels ALL executions; use `handle.Cancel()` for specific execution
7. **Modifying JAction after ExecuteAsync** - Changes don't affect running execution (task snapshot isolation)

