Skip to content

Error reference

Every error code Turbyn can return, what causes it, and how to branch on it in a workflow.

In this guide

When a run fails, Turbyn sets two output fields: errorCode (stable, safe to branch on) and errorMessage (human-readable, shown in HubSpot’s workflow history).

errorCode is the one to build logic against. Messages get reworded; codes don’t.

The codes

Code Cause
TIMEOUT The run reached the action’s configured timeout
OUT_OF_MEMORY The run exceeded its memory limit
RUNTIME_ERROR Your code threw an uncaught error
ACTION_NOT_FOUND The selected saved action is missing or unpublished
UNKNOWN Anything else

TIMEOUT

The run reached the action’s configured timeout. This can be lower than your plan’s maximum runtime. The plan ceilings are 5 minutes on Free, 30 minutes on Pro and 2 hours on Scale. See plan limits.

Usual causes: a third-party API that hung with no timeout of its own, an unbounded pagination loop, or a job that needs more time than the action allows.

Always set a timeout on outbound calls. Without one, a hanging provider consumes your whole budget:

const res = await fetch(url, { signal: AbortSignal.timeout(30_000) });

OUT_OF_MEMORY

The run exceeded its memory limit. Almost always from holding a large dataset in memory at once. A big API response, a large CSV, unbounded accumulation in a loop.

Stream rather than buffer where you can, and process in batches rather than loading everything:

// Accumulates every record before doing anything.
const all = await fetchAllRecords();
all.forEach(process);

// Bounded. Only one page is resident at a time.
for await (const page of fetchPages()) {
  page.forEach(process);
}

RUNTIME_ERROR

Your code threw and didn’t catch it. errorMessage carries the thrown message; the full stack trace is on the run detail page in Turbyn.

This is also what a deliberate throw produces, which makes it a legitimate way to fail a record on purpose:

if (!event.inputs.domain) {
  throw new Error("No domain on this record. Nothing to enrich");
}

Since that message surfaces in the workflow history, write it for the person reading the workflow.

ACTION_NOT_FOUND

The workflow step points at a saved action that can’t be resolved. Either it was deleted, or it was never published, or the step was configured against a different portal.

Check that the action still exists and has a published version. Actions that only exist as drafts don’t appear in HubSpot’s dropdown and can’t be executed.

UNKNOWN

An infrastructure-level failure that didn’t map to a specific code, such as a sandbox that failed to start. If it recurs for the same action, send us the diagnostic information from the run detail page so we can investigate.

Branching on errors in HubSpot

The Custom Code action declares execution rules on errorCode, so HubSpot’s workflow editor gives you branches per failure type without any configuration:

Custom Code
  ├─ Success        → continue
  ├─ Timed out      → notify ops, retry later
  ├─ Runtime error  → create a task for the data team
  └─ Not found      → alert an admin

Each rule renders a readable message on the workflow history page, with {{errorMessage}} interpolated.

Handling failure without failing

Often you don’t want a failure to stop the record’s journey. Return a status instead of throwing:

try {
  const data = await enrich(domain);
  return {
    status: "enriched",
    industry: data.industry,
  };
} catch (err) {
  ctx.log.warn("enrichment failed", { domain, error: err.message });
  return {
    status: "skipped",
    reason: err.message,
  };
}

Now the workflow can inspect the json output and the record keeps moving. A thrown error stops that enrollment; a returned status lets you decide what happens next.

Retries and replay

Failed runs are recorded with their full input payload, so they can be replayed from the Runs list. Replay one, or filter and replay in bulk (“all failures in the last 24 hours”).

Replay can use the version the original run executed to reproduce a historical failure, or the currently published version to run a fix. Use a test run to try an unpublished draft.

Caution

Replay re-executes the action, including its side effects. An action that charges a card or sends an email can do it again. Use ctx.executionId as an idempotency key for retries of the same execution where the destination supports one. A replay creates a new execution; to prevent duplicates across replays, use a stable key for the business operation instead.