Skip to content
Runner · ExamplesLadderrung 08

onExecuted: react to outcomes

onExecuted: set cooldown on success only

Mirrors runner/src/plugins/08-exampleOnExecuted

What this teaches

  • The onExecuted hook closes the loop: decide() returns intent, the runtime tries to submit, the hook gets called with the outcome
  • You can persist trade results, learn from failures, schedule follow-ups
  • "Set cooldown in onExecuted on success" is the cleaner pattern (vs exampleCooldown which sets it in decide())

New vs exampleConsensus

  • Lifecycle hook reacting to result.outcome
import type { StrategyPlugin } from "@automark/runtime-core";
import { Market } from "@automark/sdk/market";
import { minutes } from "@automark/sdk/duration";

const COOLDOWN_MS = minutes(20);

export default function createExampleOnExecuted(): StrategyPlugin {

  const vaultId = process.env.VAULT_ID;

  if (!vaultId) throw new Error("exampleOnExecuted: VAULT_ID not set");

  return {
    name: "exampleOnExecuted",
    vaultId,
    triggers: [{ kind: "cron", everySeconds: 60 }],

    async decide(ctx) {
      
      if (ctx.vault.isFrozen) return [{ kind: "noop", reason: "frozen" }];

      const lastSuccessMs = (await ctx.state.get<number>("lastSuccessMs")) ?? 0;

      if (ctx.now - lastSuccessMs < COOLDOWN_MS) {
        return [{ kind: "noop", reason: "cooldown (set by onExecuted)" }];
      }

      const btc = await Market.find({
        asset: "BTC",
        expiryAfterMs: ctx.now + minutes(5),
        client: ctx.suiClient,
      });

      const p = await btc.price();

      const quantity = ctx.vault.maxSinglePosition / 4n;

      if (quantity === 0n) return [{ kind: "noop", reason: "no headroom" }];

      return [
        {
          kind: "vault.mintBinary",
          params: {
            marketId: btc.id,
            strike: btc.strikeAbove(p.forwardRaw, { pctBps: 200 }),
            isUp: true,
            quantity,
          },
        },
      ];
    },

    async onExecuted(ctx, result) {
      // The runtime skips this hook entirely on dry-runs, so we never
      // see `result.dryRun === true` here.
      if (result.outcome === "submitted") {
        // Only update cooldown after the tx actually landed on-chain
        await ctx.state.set("lastSuccessMs", ctx.now);
        ctx.logger.info("trade landed", {
          digest: result.digest,
          gasMist: result.gasUsed?.toString(),
        });
      } else if (result.outcome === "failed") {
        // Don't update cooldown on failure — let the next tick retry sooner
        ctx.logger.warn("trade failed, no cooldown set", { error: result.error });
      }
    },
  };
}

// insights

// onExecuted gives you the space for when the order has been "dispatched": it may be submitted or it may hit some error along the way; either way, onExecuted lets you handle, diagnose, and refine your strategies.
Go deeperLifecycle hooksonExecuted and onError react to the result of each tick without breaking the next one.

Environment variables

  • VAULT_ID