Actions
Apps/Build/Actions

Actions

Actions are the code that changes records. Each runs in the app's own deployment; its changes to its record are saved in one transaction when it returns, then onSuccess runs.

Action files

A model's actions are files in api/models/<model>/actions/; the file name is the action's name. Each exports run, and optionally onSuccess and options.

import { applyParams, save, type ActionOnSuccess, type ActionOptions, type ActionRun } from "@inferencebureau/apps-sdk";

/** A shopper's review. It always starts pending, whatever the caller sent. */
export const run: ActionRun = async ({ params, record, api, logger }) => {
  applyParams(params, record);
  record.status = "pending";
  record.rating = Math.round(Number(record.rating));
  // Link the synced product when the app syncs shopifyProduct.
  const product = await api.shopifyProduct.findFirst({ filter: { shopifyId: { equals: record.productGid } } });
  record.product = product?.id ?? null;
  await save(record);
};

export const onSuccess: ActionOnSuccess = async ({ record, logger }) => {
  logger.info("New review waiting for approval", { id: record.id, rating: record.rating });
};

export const options: ActionOptions = {
  actionType: "create",
};
OptionMeaning
actionTypecreate, update, delete or custom. Without it, an action named create, update or delete is that type and anything else is custom.
returnTypetrue sends what run returns back to the caller as result. Otherwise it's dropped.
descriptionOne line on what the action does, for the app's API reference, its generated types and its MCP server. Each param takes a description too. Like params, they're read from the file, so keep options a plain object literal.
timeoutMSHow long run and onSuccess may take together, at most 15 minutes: 30 seconds unless it says, or five minutes as a background job. onSuccess gets what run left, and at least a second. Past a minute or so, prefer a job: a caller waiting on the answer may give up first.

Options are read from the file's text, so write them as literals in export const options = { … }.

Action types

Typerecord isCalled with
createA new record, with only its id so farNo id
updateThe stored recordIts id
deleteThe stored record. Call deleteRecord(record) to remove itIts id
customThe stored recordIts id

The context

run and onSuccess receive one object:

PropertyWhat it is
paramsWhat the caller passed. A model action's fields may be at the top level or nested under the model's name ({ review: { … } }).
recordThe record the action works on (null in a global action).
apiThe app's client, as the environment. See Reading and writing records.
loggerdebug, info, warn and error, each (message, data?). Lines appear on the run in Logs.
shopThe store the run is about (an install, a webhook, or the caller's shop), or null.
shopifyThe Admin API for that store, or null. See The Admin API.
aiModels through Inference Bureau's gateway, billed to the team: ai.generateText, ai.embed, or the AI SDK. See Calling models.
signalAn AbortSignal that fires when the run is stopped: its job canceled, or its time up. Pass it to fetch and check signal.aborted in long loops to stop early.
model, action, environmentWhere the run is: the model (null for a global action), the action's name, and development or production.

The action's record

Nothing is written from the app while run runs. Change the record's fields, then mark what should happen to it:

  • applyParams(params, record) copies the params onto the record (except id, createdAt and updatedAt).
  • await save(record) saves it when run returns. A new record saves every field you set; an existing one saves only the fields that changed.
  • await deleteRecord(record) deletes it when run returns.

When run returns, Inference Bureau applies the save or delete in one transaction, with the model's validations. If run didn't call either, the record is left as it was. save in onSuccess writes nothing, and a warning in the run's logs says so.

Writing other records with the run

save and deleteRecord also take records the action's api read (findOne, findFirst, findMany, search), and new ones made with api.<model>.build(values). They're written with the action's own record in the same transaction, in the order they were saved: all of them, or none when one is refused or run throws. A built record has its id at once, so other records can point at it, and so does a create action's own record.

import { applyParams, save, type ActionRun } from "@inferencebureau/apps-sdk";

/** An order and its lines, and the stock they reserve: all written, or none. */
export const run: ActionRun = async ({ params, record, api }) => {
  applyParams({ customer: params.customer }, record);
  await save(record);
  for (const line of params.lines as { sku: string; quantity: number }[]) {
    await save(api.orderLine.build({ order: record.id, sku: line.sku, quantity: line.quantity }));
    const stock = await api.stockLevel.findFirst({ filter: { sku: { equals: line.sku } } });
    if (!stock || Number(stock.available) < line.quantity) throw new Error(`${line.sku} is out of stock`);
    stock.available = Number(stock.available) - line.quantity;
    await save(stock);
  }
};
  • These are record writes, checked against each model's schema. They don't run the other model's actions; api.<model>.run() does, outside the transaction.
  • An update writes only the fields changed since the record was read, without locking it while run works. Two runs adding to the same counter can both read 4 and write 5.
  • For a run about one store, a store-owned model's new records are that store's, and updates and deletes reach only its records.
  • At most 500 other records per run. Queue a job per batch for more.
  • api.<model>.create, update and delete still write at once, outside the transaction, and stand even if the action later fails.

A model's create, update and delete actions also run when a record is created, updated or deleted through the API, the client or the MCP server, so they are the place for rules every write must follow.

onSuccess

onSuccess runs after the record is committed, with the saved record. Use it for work that should only happen once the change stands: Shopify writes, calls to other services, model calls. If it throws, the run is marked partial in Logs and the record's changes stay.

Global actions

Actions that don't belong to one model live in api/actions/. Their record is null; use api to read and write.

// api/actions/weeklyDigest.ts
import type { ActionOptions, GlobalActionRun } from "@inferencebureau/apps-sdk";

export const run: GlobalActionRun = async ({ params, api, logger }) => {
  const page = await api.review.findMany({ filter: { status: { equals: "pending" } }, first: 250 });
  logger.info("Pending reviews", { count: page.records.length });
  return { pending: page.records.length };
};

export const options: ActionOptions = { returnType: true };

Calling an action

From the app's code, run an action with the client. From outside, use the REST route, the MCP server's run_action tool, or Run action on the action file in the dashboard.

import { createClient } from "@inferencebureau/apps-sdk";

const api = createClient();

// A model action on a record. shop gives the action its shopify client for that store.
const { record } = await api.review.run("approve", { id: "rec_…", shop: "inference-bureau.myshopify.com", params: { moderationNote: "Thanks!" } });

// A create action makes a new record.
await api.review.run("create", { params: { productGid, shop, rating: 5, body: "Fits well, washes well.", authorName: "Sam" } });

// A global action.
const { result } = await api.$global("weeklyDigest");

The answer is the record as the action left it (null after a delete), result when the action returns one, and event, the run's id in Logs. shop must be a x.myshopify.com domain the environment is installed on.

How actions run

Actions run inside the app's own deployment, not in a sandbox per call. Every build copies api/ into the deployment, generates a registry of its action files, and adds a route, /_ib/actions. To run an action, Inference Bureau calls that route on production's deployment or development's dev server, signed with the environment's token; the route answers 404 to anything unsigned.

  1. Inference Bureau sends the action, its params and its record. The app runs run and answers with what it asked to happen to the record, what it returned, and its log lines.
  2. Inference Bureau applies the write in one transaction through the records engine.
  3. It calls the app again for onSuccess with the saved record.

A run is one action event in Logs, with its log lines, its params and the write under it. A new action file is live in development as soon as it syncs; in production, after the next deploy.

Failures

What happenedResult
run threwNothing is written. The caller gets 422 action_failed with the message, the run's event and its logs. Logs show the stack.
run finished but a write was refusedNothing is written, neither the record nor the others run saved. The caller gets 400 with each field's problem, and the run is logged as failed.
onSuccess threwThe write stands. The run is logged as partial with the error.
The log has warningsThe run succeeds, and its row in Logs counts the warnings, so a refused Shopify write isn't hidden inside a success.
The app isn't answeringThe caller gets 503. A sleeping dev server is woken; try again in a minute. Production must be deployed first.
It took too longThe run fails after its timeoutMS, and the caller gets 503.