JS Webhooks

Webhook endpoints serve HTTP requests. The handler receives a Request object and must return a Response. Webhooks can call activities and workflows via imports or obelisk.call and make outbound HTTP requests via fetch. See Webhook Endpoints for the general model.

Prefer imports when the target function is known statically. Imports are verified when the deployment starts; use dynamic FFQN calls only when the target is constructed at runtime.

[[webhook_endpoint_js]]
name     = "my_webhook"
location = "webhook/my_webhook.js"
routes   = [
    { methods = ["GET"],  route = "/items" },
    { methods = ["POST"], route = "/items/:id" },
]
env_vars = [{ key = "DB_URL", value = "..." }]
[[webhook_endpoint_js.allowed_host]]
pattern = "https://api.example.com"
methods = ["GET"]
[webhook_endpoint_js.allowed_host.secrets]
env_vars = ["API_KEY"]
replace_in = ["headers"]

Route path segments starting with : are captured and exposed as process.env entries inside the handler — process.env['segment-name'] returns the captured value as a string.

Handler signature

export default function handle(request) {
  // sync or async
  return new Response("body", { status: 200, headers: { "content-type": "text/plain" } });
}

Request object

Property / methodDescription
request.urlFull request URL as a string
request.methodHTTP method ("GET", "POST", …)
request.headersHeaders object
request.headers.get(name)Returns header value or null; multiple values joined by ", "
await request.text()Read the request body as a string
await request.json()Parse the request body as JSON
await request.formData()Parse the request body as form data

Reading the request body

export default async function handle(request) {
  // As plain text
  const text = await request.text();

  // As JSON
  const data = await request.json();

  // As form data (application/x-www-form-urlencoded or multipart/form-data)
  const form = await request.formData();
  const field = form.get("field-name");

  return Response.json({ received: data });
}

Response construction

// Text response
return new Response("Hello!", { status: 200, headers: { "content-type": "text/plain" } });

// JSON response (sets content-type: application/json automatically)
return Response.json({ key: "value" });
return Response.json([1, 2, 3], { status: 201 });

// Proxy a fetch response directly
const resp = await fetch("https://api.example.com/data");
return resp;

Calling activities and workflows

Import functions by their WIT interface path. The call blocks until the child execution completes, so the HTTP response is only sent after the child finishes.

import { serial } from "myapp:demo/workflow";

// ffqn: myapp:demo/webhook.handle
export default function handle(request) {
  const url = new URL(request.url);
  if (url.pathname === "/serial") {
    const result = serial();
    return new Response(`serial completed: ${result}`, { status: 200 });
  }
  return new Response("not found", { status: 404 });
}

For FFQNs constructed at runtime, use obelisk.call(ffqn, argArray) directly.

Path parameters

When a route contains named segments (:name), the captured values are available as process.env entries inside the handler:

[[webhook_endpoint_js]]
name    = "my_webhook"
location = "webhook/my_webhook.js"
routes  = ["/users/:user-id/items/:item-id"]
export default function handle(request) {
  const userId = process.env["user-id"];
  const itemId = process.env["item-id"];
  return Response.json({ userId, itemId });
}

Scheduling executions

Webhooks can schedule top-level executions without blocking the HTTP response. Import from the -obelisk-schedule suffix:

import { sendEmailSchedule } from "myapp:demo-obelisk-schedule/activity";

export default function handle(request) {
  const execId = sendEmailSchedule(null, "user@example.com");
  // With a delay: sendEmailSchedule({ seconds: 60 }, "user@example.com");
  return new Response(execId, { status: 202 });
}

For FFQNs constructed at runtime, use obelisk.schedule directly:

const execId = obelisk.executionIdGenerate();
obelisk.schedule(execId, "myapp:demo/activity.send-email", ["user@example.com"]);

Check or retrieve a previously scheduled or running execution:

const status = obelisk.getStatus(execId);
// e.g. { status: "pendingAt"|"locked"|"paused"|"blockedByJoinSet"|"cancelling" } or { status: "finished", finishedStatus:"ok"|"err"|"executionFailure" }

const result = obelisk.tryGet(execId);
// undefined if not finished yet
// returns ok value when done, or throws obelisk.ChildExecutionError on failure

Get the execution ID of the current webhook invocation:

const myExecId = obelisk.executionIdCurrent();
// Returns the execution ID string for this webhook handler invocation

Webhook API

Import-based calls (preferred):

Import patternDescription
import { fn } from "ns:pkg/ifc"Call and await a child execution
import { fnSchedule } from "ns:pkg-obelisk-schedule/ifc"Schedule a fire-and-forget execution

Dynamic calls (when FFQN isn't known statically):

CallReturnsDescription
obelisk.call(ffqn, argArray)returns ok value, throws obelisk.ChildExecutionError on failureCall and await a child execution
obelisk.executionIdGenerate()executionId (string)Generate a unique execution ID
obelisk.executionIdCurrent()executionId (string)Get the execution ID of the current webhook invocation
obelisk.schedule(execId, ffqn, argArray)Schedule a top-level execution immediately
obelisk.schedule(execId, ffqn, argArray, delay)Schedule at a delay (e.g. { seconds: 60 })
obelisk.getStatus(execId){ status: "pendingAt"/"locked"/"paused"/"blockedByJoinSet"/"cancelling"/"finished", finishedStatus:"ok"/"err"/"executionFailure" }
obelisk.get(execId)ok value (throws obelisk.ChildExecutionError on failure)Blocking: waits until execution finishes
obelisk.tryGet(execId)ok value (throws obelisk.ChildExecutionError on failure), undefined if not finished yetNon-blocking result fetch

Available globals

Same as JS activities: process.env, fetch, crypto.subtle, console, TextEncoder/TextDecoder.

Fetch example

export default async function handle(request) {
  const someHeader = request.headers.get("x-custom");
  const api_key = process.env["API_KEY"]; // JS code only gets a short-lived token, transformed on outbound HTTP request.
  // Requires [[webhook_endpoint_js.allowed_host]] for the target
  const resp = await fetch("https://api.example.com/data", {
    headers: { accept: "application/json", authorization: `Bearer ${api_key}` },
  });
  return resp;
}