Skip to content

hailer.action ​

Trigger workspace actions from your app and read back what they returned. An action is user-authored code that a workspace admin creates in Hailer's workspace settings (Settings → Actions). It runs in Hailer's sandbox, with the workspace's bound credentials injected as environment variables, and its result is recorded as a run.

Apps cannot create, edit, or list actions. They trigger actions that already exist and that the acting user is allowed to run.

MethodHost opReturns
trigger(target, input?)action.triggerPromise<ActionRun>
getRun(runId)action.getRunPromise<ActionRun>

Each host op proxies to the backend's v3.action.trigger and v3.action.getRun, as the current user.

Who may trigger an action ​

The backend decides per call:

  • Workspace admins and owners may trigger any action in their workspace.
  • Other users may trigger an action only if they match its runners — a list the admin sets on the action: the whole workspace, teams, groups, or individual users. An action with no runners is admins-only.

Anyone else gets Access denied (403). Disabled actions refuse to run for everyone.

When you build an app around an action, ask the workspace admin to add your app's users (or the whole workspace) as runners of that action.

Addressing an action ​

trigger takes a target with one of actionId or key, and an optional workspaceId:

ts
// By id — the action's _id, shown in the action editor
await hailer.action.trigger({ actionId: '6adb4765aaf01c9ea7306ed7' });

// By key — a stable handle the admin sets on the action, unique within the workspace
await hailer.action.trigger({ key: 'sync-crm' });

// Private app: a key needs the workspace it lives in
await hailer.action.trigger({ key: 'sync-crm', workspaceId });

Prefer the key form in apps that are installed into more than one workspace: the admin of each workspace creates an action with the agreed key, and your code stays the same. Keys are lowercase letters, digits and dashes, up to 64 characters.

In a workspace app the host fills in workspaceId from the app's own workspace, so you can leave it out. In a private app there is no such default: pass the workspaceId of the workspace whose action you mean, or the call fails with WorkspaceId is required with key. See Private vs workspace apps.

Actions can also be made public by the admin, which gives them a webhook URL that anyone can call without logging in. That path is not exposed through the SDK — use http.fetch or a plain fetch to the webhook URL if you need it.

trigger(target, input?) ​

ts
const run = await hailer.action.trigger(
  { key: 'sync-crm' },
  { orderId: activity._id, reason: 'manual sync' },
);

if (run.status === 'completed') {
  console.log(run.output);
}

input is any JSON-serializable value (null when omitted). The action receives it as the payload of a manual event:

ts
// Inside the action's code
export async function execute(input) {
  // input = { event: 'manual', payload: <your input>, workspaceId, triggeredBy }
  return { ok: true, received: input.payload };
}

triggeredBy is the id of the user who triggered the run, i.e. the user of your app. The action's return value comes back to you as run.output.

What to expect: the 25 s wait ​

The host holds the request for up to 25 seconds:

  • A run that finishes within that window resolves with its final record: status is a terminal value and output, message, wallTimeMs and finishedAt are filled in.
  • A longer run resolves with the record still queued or running. The run keeps going in the background. Poll getRun with run._id until the status is terminal.
ts
import { ACTION_RUN_ACTIVE_STATUSES } from '@hailer/app-sdk';

let run = await hailer.action.trigger({ actionId });

while (ACTION_RUN_ACTIVE_STATUSES.includes(run.status)) {
  await new Promise(resolve => setTimeout(resolve, 2000));
  run = await hailer.action.getRun(run._id);
}

Design actions you trigger from an app to finish well under 25 s where you can; the polling path is for long jobs, not the default.

Run statuses ​

statusMeaning
queuedAccepted, not started yet. Still in progress.
runningExecuting in the sandbox. Still in progress.
completedexecute() returned. Its return value is in output.
runtime_errorThe code threw or exited with a non-zero code. See message.
timeoutThe run exceeded the action's wall-time limit.
output_limitstdout, stderr or the return value exceeded the size cap.
infra_errorThe sandbox failed. Not the action's fault; retry later.
cancelledA queued run was stopped before it started.
config_errorThe action's configuration is broken, for example a bound credential no longer exists. The admin has to fix the action.
deniedPolicy refused the run, for example the workspace is out of tokens or the action hit its runs-per-hour limit.

Only queued and running are non-terminal; ACTION_RUN_ACTIVE_STATUSES holds exactly those two.

What you see in the run record ​

What trigger and getRun return depends on who the acting user is:

FieldAdmin / ownerRunner
_id, actionId, workspaceId, event, triggeredBy, status, exitCode, output, message, wallTimeMs, created, finishedAt✓✓
arguments, stdout, stderr, execRunId✓—

Runners get the outcome only. They never see the action's code, its logs, or the exact arguments. Build your app on status, output and message, and treat the admin-only fields as optional debugging extras.

getRun(runId) ​

ts
const run = await hailer.action.getRun(runId);

Returns one run record in the same shape as trigger. Admins can read any run in their workspace; other users can only read runs they triggered themselves, and only runs of saved actions (not editor test runs).

Errors ​

Every method throws HailerError.

SituationWhat you see
Action id or key does not existAction not found (404)
User is neither admin nor a runner of the actionAccess denied (403)
Run id does not existRun not found (404)
Reading someone else's run as a non-adminAccess denied (403)
Both or neither of actionId / key givenValidation error from the host
key in a private app without workspaceIdWorkspaceId is required with key.
Actions disabled on the instanceActions are disabled (500)

A run that ran but failed is not an error: trigger resolves normally with a runtime_error, timeout, config_error or denied status. Check status, not just the promise.

Rate limits ​

Runs count against the action's runs per hour limit (default 1000, set by the admin). A trigger past the limit resolves with status denied. Triggers are also rate-limited per user like other API calls.

Example: a button that runs an action and shows the result ​

tsx
import { ACTION_RUN_ACTIVE_STATUSES, ActionRun, HailerError } from '@hailer/app-sdk';

async function runSync(activityId: string): Promise<ActionRun> {
  let run = await hailer.action.trigger({ key: 'sync-crm' }, { activityId });

  while (ACTION_RUN_ACTIVE_STATUSES.includes(run.status)) {
    await new Promise(resolve => setTimeout(resolve, 2000));
    run = await hailer.action.getRun(run._id);
  }
  return run;
}

try {
  const run = await runSync(activity._id);
  if (run.status === 'completed') {
    hailer.ui.snackbar.open(`Synced: ${JSON.stringify(run.output)}`, 'OK');
  } else {
    hailer.ui.snackbar.open(`Sync ${run.status}: ${run.message ?? ''}`, 'OK');
  }
} catch (error) {
  const e = error as HailerError;
  hailer.ui.snackbar.open(e.code === 403 ? 'You may not run this action' : e.message, 'OK');
}

Related: credential for secrets the action itself needs (bound to the action by the admin, not by the app), and http for calling a public action's webhook URL.

Hailer Developer Documentation · v1.8.0