Foundations

Host functions

Learn how sandboxed code calls functions in the host application.

Host functions

Host functions are functions that guest code is allowed to call. They are the only supported way for source running in the sandbox to interact with your application or the systems connected to it.

A host function should expose one narrow capability. Rather than handing the guest a database client, expose a function that loads one kind of record, after the application's validation and authorization rules have run.

Defining host functions

Host functions are defined under a namespace. The namespace becomes a global object in the sandbox, and each host function becomes a method on that object.

import { run } from 'run';

const result = await run({
  source: `
    const user = await users.find("user_123");
    return user;
  `,
  hostFunctions: {
    users: {
      find: async (id: string) => {
        return { id, name: 'Ada' };
      },
    },
  },
});

The fully qualified name of the host function in this example is users.find. The users namespace is available only for this invocation.

A namespace must be a valid JavaScript identifier, must not conflict with a reserved global, and must not begin with __run.

Calls

Calling a host function creates a request from the guest to the host. The arguments are serialized, copied to the host, and passed to the host function as ordinary function arguments. The returned value is then serialized and copied back to the guest.

Host functions may return values directly or return promises:

const hostFunctions = {
  math: {
    double: (value: number) => value * 2,
  },
  users: {
    find: async (id: string) => {
      return { id, name: 'Ada' };
    },
  },
};

Guest code should await every host function call. Returning while host function work is still pending can fail the invocation because the runtime cannot safely detach host work from the guest that requested it.

await events.record({ name: 'opened' });
return { recorded: true };

Several host function calls can run concurrently when the guest awaits them together:

const [profile, orders] = await Promise.all([
  users.profile(userId),
  orders.list(userId),
]);

The invocation limits both the total number of host function calls and the number that may be in flight at once.

Synchronous host functions

Use syncHostFunctions when a guest API must return before the next guest statement executes. Synchronous bindings are configured on a runner and exposed as ordinary, non-promise-returning guest methods:

import { createRunner } from 'run';

const files = new Map<string, string>();

const runner = createRunner({
  syncHostFunctions: {
    fs: {
      readFile(path: string) {
        return files.get(path);
      },
      writeFile(path: string, value: string) {
        files.set(path, value);
      },
    },
  },
});

const result = await runner.run({
  source: `
    fs.writeFile('/state', 'ready');
    return fs.readFile('/state');
  `,
});

The host implementation may itself be asynchronous. The worker remains blocked until it settles, while guest code receives either the serializable return value or an immediate throw. A synchronous binding cannot interrupt a run.

Synchronous and asynchronous namespaces must not overlap. Both kinds of calls share maxBridgeRequests; synchronous requests execute one at a time and do not count toward maxInFlightBridgeRequests.

The synchronous bridge is created only for runners that expose synchronous bindings or runs that use a module loader. Its allocation is included in memory admission and capped at 64 MiB. SharedArrayBuffer and Atomics remain unavailable to guest code.

Synchronous outcomes are recorded in the continuation ledger. Replay returns a recorded value or throws the recorded error without invoking the binding again; calls first reached after the recorded frontier dispatch normally. The synchronous binding-name manifest is part of continuation scope, so changing it invalidates an existing continuation before replay.

Context

A host function can call getHostFunctionContext() to read information about its active request:

import { getHostFunctionContext } from 'run';

const hostFunctions = {
  users: {
    find: async (id: string) => {
      const context = getHostFunctionContext();

      return {
        id,
        logicalRunId: context.logicalRunId,
        name: 'Ada',
      };
    },
  },
};

getHostFunctionContext() exposes the following information and controls:

  • abortSignal lets the host function stop its work when the run is cancelled, times out, or fails.
  • invocationId identifies the current execution attempt. It changes when an interrupted run is replayed.
  • logicalRunId identifies the complete workflow and remains stable across replay attempts.
  • requestId identifies the current host function call within the invocation.
  • requestIndex contains the one-based order of the host function call.
  • hostFunctionName contains the fully qualified host function name, such as users.find.
  • interrupt(payload) pauses the run and returns an application-defined payload to the host.
  • resume is available when an interrupted host function is invoked again. It contains the interruption ID, original payload, and supplied resolution.

Use these identifiers to trace, audit, and deduplicate external side effects. They are not authentication credentials.

Cancellation

The host function context includes an AbortSignal. It becomes aborted when the invocation is cancelled, times out, or otherwise fails.

Host functions should pass this signal to operations that support cancellation:

const hostFunctions = {
  search: {
    query: async (query: string) => {
      const { abortSignal } = getHostFunctionContext();

      return searchClient.query(query, {
        signal: abortSignal,
      });
    },
  },
};

This requirement also applies to synchronous bindings whose host implementation returns a promise. Although the guest worker is blocked, the host operation can still observe cancellation and stop its work.

Aborting the signal cannot automatically undo work that has already completed. Host functions that perform writes should use application-level idempotency and deduplication.

Failures

When a host function throws, its guest-side promise rejects. Guest code may catch the error and continue:

try {
  return await inventory.reserve('item_123');
} catch (error) {
  return {
    reserved: false,
    message: error.message,
  };
}

The guest receives a sanitized error without the host stack or private diagnostic details. The host should record the complete error before rethrowing it when those diagnostics are needed.

Host functions are trusted code. They should validate every argument, enforce authorization in the host, and return only the data required by the source.