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 obelisk.call and make outbound HTTP requests via fetch. See Webhook Endpoints for the general model.

[[webhook_endpoint_js]]
name     = "my_webhook"
location = "${DEPLOYMENT_DIR}/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

Webhooks have access to obelisk.call — the call blocks until the child execution completes, so the HTTP response is only sent after the workflow finishes.

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

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 = "${DEPLOYMENT_DIR}/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. First generate an execution ID, then pass it to obelisk.schedule:

export default function handle(request) {
  const execId = obelisk.executionIdGenerate();
  obelisk.schedule(execId, "myapp:demo/activity.send-email", ["user@example.com"]);
  // optional schedule-at: obelisk.schedule(execId, ffqn, args, { seconds: 60 });
  return new Response(execId, { status: 202 });
}

Check or retrieve a previously scheduled or running execution:

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

const result = obelisk.tryGet(execId);
// { pending: true } if not finished yet
// { ok: value } or { err: value } when done

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

CallReturnsDescription
obelisk.call(ffqn, argArray)returns ok value, throws error value
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"/"finished", finishedStatus:"ok"/"err"/"executionFailure" }
obelisk.get(execId){ ok/err: value }Blocking: waits until execution finishes
obelisk.tryGet(execId){ pending: true } or { ok/err: value }Non-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;
}