HighX
Server

Actions

The one place the outside world happens — HTTP, model providers, global storage.

An action is for everything a mutation may not do: HTTP calls, model providers, payment APIs, and writes to global storage.

import { action } from "@highx/core";

export const enrich = action({
  name: "notes.enrich",
  args: domainArgs,
  returns: enrichment,
  auth: can("notes:write"),
  handler: async (ctx, args) => {
    const response = await ctx.fetch(`https://api.example.com/${args.domain}`);
    return ctx.run(saveEnrichment, { domain: args.domain, value: await response.json() });
  },
});

What an action can reach

ctx.fetchHTTP, with the request deadline already attached
ctx.run(mutation, args)Run a mutation as its own transaction
ctx.dbRead tenant data directly
ctx.globalThe D1 read model
ctx.sagaA durable, replayable sequence of steps
ctx.signalAborts when the deadline passes

An action is not a transaction

This is the one thing to internalize. ctx.run commits immediately; if the action fails afterwards, that write stays.

Three shapes, of which two are safe on a retry:

ShapeOn retry
Same idempotency key, first attempt succeededThe stored result is replayed; the handler never runs again
ctx.run inside a saga stepThe step replays its output, or was compensated and re-runs clean
Bare ctx.run, no saga and no keyRepeats. Nothing recorded the earlier commit, so a second row is written

The third is a real hazard, not a theoretical one. Give any committing action a saga step or an idempotency key.

Deadlines

ctx.signal aborts when the request deadline passes, and ctx.fetch already carries it. Pass the signal to anything else you await, so a slow provider ends the action rather than holding the object.

Actions are not live

An action returns a value to its caller and nothing else. It does not create a subscription, and it is not re-run on data changes. If a UI should update as a result, the mutation the action ran is what patches the subscribers.

Next

On this page