AI Knowledge & Logic

JavaScript Sandbox

Write custom actions, condition providers, event handlers, recurring jobs, sidebar widgets and prompt sections in TypeScript instead of Python.

These features — custom actions, condition providers, event handlers, recurring jobs, sidebar widgets and their actions, and prompt sections — accept TypeScript as well as Python. Pick the language in the editor (or pass language: "javascript" through the API/MCP); everything else about the feature is the same.

Custom HTTP endpoints use JavaScript/TypeScript only, with no language selector. Export handle(context), type its argument as HttpEndpointContext, and return JSON or the synchronous httpResponse(body, { status, headers }) helper from "octocom". The request is available at context.request; endpoints have a 30-second execution deadline.

The two languages have the same helpers, the same context, the same entry-point names and the same security model. This page covers what is different.


The shape of a JavaScript automation

import { addConversationTag, getSecret } from "octocom";
import type { ExecutionContext } from "octocom";

export async function execute_action(context: ExecutionContext) {
  const email = context.args?.email as string | undefined;
  if (!email) return { error: "email is required" };

  const key = await getSecret("CRM_API_KEY");
  const res = await fetch(
    `https://crm.example.com/customers?email=${encodeURIComponent(email)}`,
    {
      headers: { Authorization: `Bearer ${key}` },
    },
  );
  if (!res.ok) return { error: `CRM returned ${res.status}` };

  await addConversationTag(context, "crm-matched");
  return await res.json();
}

Three rules:

  1. Export the entry function. The name is the same as in Python (execute_action, evaluate_conditions, handle_event, run_job, get_sidebar_data, build_prompt_sections) and it must be exported. It may be async and normally is, because every helper returns a Promise.
  2. Import helpers from "octocom". Nothing is global. Helper names are the camelCase form of the Python ones: add_conversation_tagaddConversationTag, get_secretgetSecret.
  3. Return a JSON-serialisable value. undefined becomes null. Anything that can't be serialised (a Response, a cyclic object) fails the run with your code on the stack.

Only two kinds of import exist: "octocom" and your organization's own modules as "modules/<name>". There is no npm; an import of anything else is rejected before the code runs.


The context object

Identical fields to the Python context, with two spelling differences: keys that were snake_case in Python are camelCase (browser_sessionbrowserSession), and "absent" is null, never undefined.

FieldTypeNotes
conversationConversationContext | nullnull outside a conversation (recurring jobs, the test harness without a conversation, some events)
business{ name, slug }Always present
customerCustomerContext | nullnull until the customer is identified
argsRecord<string, unknown>Action / condition-provider arguments, event payload (event_type and friends)
workflow{ id, slug, title }Condition providers only
job{ id, name, schedule, ... }Recurring jobs only; keys are snake_case exactly as documented for Python
browserSessionunknownWeb chat only

ExecutionContext, ConversationContext and CustomerContext are exported from "octocom" for typing.


Helpers

Every helper takes the same arguments as its Python counterpart, in the same order, and returns a Promise of the same value. Optional trailing keyword arguments become an options object.

Errors are thrown, not returned: a helper whose REST call fails throws OctocomApiError (with .status, .url, .body); getSecret throws SecretNotFound; the organization store throws OrganizationStoreConflictError (with .currentRevision) on a revision mismatch. All three are exported from "octocom" for instanceof checks.


Organization modules

Shared code lives in the same place as Python modules — Settings → Modules — with the language set to TypeScript. Unlike Python modules, which become globals, JavaScript modules are imported explicitly and must export what they share:

// module "crm"
export async function lookupCustomer(email: string) {
  /* ... */
}
import { lookupCustomer } from "modules/crm";

A module is only visible to code in its own language, and a broken JavaScript module only breaks the code that imports it.


Logging and errors

console.log / console.warn / console.error output is captured and shown in the test harness and execution logs, in place of Python's stdout. Stored secret values are stripped from it before anything is persisted, exactly as for Python — but do not print them on purpose.

A thrown error fails the run. The error type, message and the line in your code are shown in the editor; the stack trace is available in the details view.


Limits and behaviour

JavaScriptPython
RuntimeCloudflare Workers (V8 isolate)Azure Dynamic Sessions (container)
Wall-clock timeout60 s (prompt sections: a few seconds)same
CPU time30 s per run; time spent awaiting fetch is freecounts toward wall clock
Memory128 MBmore
Concurrencyunlimited — every run is its own isolateruns of one organization queue behind a small number of containers
Outbound HTTPfetch; up to 1000 requests per runrequests; unlimited
Module-level statemay survive between runs of the same code; never rely on it, never store per-conversation data theresame caveat, and shared across all of an organization's code
Packagesnone beyond "octocom" and organization modulesPython standard library + requests

JavaScript is the better fit for anything that is mostly I/O — calling APIs, reading and writing conversation state — and for anything that runs often or in bursts (condition providers, prompt sections, event handlers). Python remains the choice for CPU- or memory-heavy work and for product-sync parsers, which are Python-only.


Security

The same guarantees apply in both languages:

  • Code can only reach your organization's data. Python runs with your organization's API key; JavaScript runs with a short-lived token minted for that one execution and revoked when it returns. The token is held by the runtime, not your code — helpers and octocomApi use it on your behalf.
  • Secret values never leave the sandbox in logs, stack traces or return values.
  • Helpers that change conversation state are restricted to the execution types where that is safe (handOffConversation in bot actions; sendBotMessage / closeConversation in event handlers, recurring jobs and sidebar actions). The restriction is enforced by the runtime from the real execution type, not from anything your code passes.
  • Each piece of code runs in its own isolate. No other organization's code ever shares a process with yours.
  • Requests through proxyFetch cannot target private or internal addresses.

On this page