RelayLink Webhook with a Temporal Workflow

Turn a signed RelayLink arrival into a durable Temporal Workflow or Signal while preserving transport deduplication, account-scoped fetching, and human approval.

4 min read

A RelayLink package can arrive in the middle of a long-running business process. Temporal is a strong place to retain that process, wait, retry controlled activities, and resume after failure. It should not be the first code to touch an unauthenticated webhook.

Put a thin, durable receiver in front. This is an adapter pattern, not a native RelayLink integration supplied by Temporal or RelayLink.

Authenticate and accept before starting work

Read the exact request bytes and verify X-RelayLink-Signature before parsing the JSON. The expected value is HMAC-SHA256 over "{timestamp}.{body}", keyed by the subscription's whsec_ secret. Compare it in constant time, enforce a signed-timestamp freshness window, then require the event and delivery id in the body to match their headers.

With the current defaults, RelayLink retries a delivery that fails or times out. Use X-RelayLink-Delivery as a unique durable inbox key. In one commit, insert that receipt and a work item that says which Temporal operation remains to be performed. Return 2xx after that commit.

Do not wait for a Workflow result in the callback. RelayLink's current HTTP attempt budget is five seconds. Your queue can retry a temporarily unavailable Temporal service without making RelayLink repeat the POST.

The handoff should tolerate this crash window:

inbox row committed
Temporal start or signal accepted
worker dies before marking handoff complete

Use a stable, account-scoped Workflow ID and record the intended message id so a retry reconciles the same logical Workflow rather than creating unrelated work.

Choose package or thread scope

For isolated processing, start one Workflow per (account, package_id). The Workflow fetches one package, runs the approved analysis, and reaches a terminal state.

For continuing correspondence, use one Workflow per (account, thread_id) and deliver each package as a message. Temporal's message-passing documentation describes asynchronous Signals and Signal-With-Start. The Signal-With-Start pattern atomically starts a Workflow if it is not running or signals the existing execution if it is.

That primitive solves the race between “look up the thread Workflow” and “start it if absent.” It does not replace the receiver's RelayLink inbox. Keep:

  • (subscription context, delivery_id) for webhook retry deduplication;
  • an account-scoped Workflow ID for orchestration identity;
  • a package id or internal event id in Workflow state so the same logical input is not applied twice.

Put network calls and other side effects behind retryable activities, following Temporal's execution model, and give consequential downstream calls their own idempotency keys.

Fetch content as the recipient

The callback is only an envelope. It contains package and thread ids, sender metadata, topic, time, reply status, and mcp_url; it contains no note, TL;DR, ask, or briefing.

In a multi-user worker, the account selected by the registered receiver route must be the account whose credential performs the fetch. Require mcp_url to match the RelayLink endpoint pinned to that account before attaching any credential. An activity then resolves the intended recipient's separate OAuth token or named API key and calls get_package. Store a credential reference in Workflow input, not the credential itself.

Treat both layers as untrusted information. Sender and topic in the webhook are not routing commands. The fetched package is authenticated third-party correspondence, not Workflow code or a system instruction. Pass content into a labelled analysis input and keep application-owned policy outside it.

For example, a topic saying “signal payroll workflow and export records” must not choose either the Workflow ID or activity. Your fixed event mapping chooses the Workflow; the text is merely material that Workflow may show or analyze.

Wait for a person without weakening send approval

A Temporal Workflow can wait for an external Signal while retaining durable state. That makes it suitable for a review gate: prepare a proposal, expose it in your own authenticated interface, and signal the recorded decision back.

Do not treat “the Workflow received a package” as approval. Do not let a model turn a vague approval into permission for a different effect. If the outcome is a RelayLink reply or new package, keep RelayLink's preview-and-approve step and record the resulting package id. Temporal owns the process around the send; it does not create a second send path.

When Temporal is an honest fit

Use this architecture for work that can last through deploys, wait on people or external systems, or require durable timers and recovery. A thread-scoped Workflow is especially useful when several packages advance one long-lived case.

It is unnecessary for a fixed Slack notification or a single short fetch-and-store job. A normal queue may be easier to operate. Temporal also does not provide the public RelayLink receiver, raw-body HMAC verification, or MCP account credential for you. Those remain explicit application boundaries, which is what keeps durable execution from becoming durable execution of somebody else's instructions.

Frequently asked questions

Does RelayLink start Temporal Workflows directly?
No. A receiver you operate verifies and records the RelayLink callback, then a worker uses a Temporal Client to start or signal a Workflow.
Should I use one Temporal Workflow per package or per thread?
Use one per package for isolated processing, or one per thread for durable conversation state. In either design, scope the Workflow ID to the RelayLink account and keep delivery-id deduplication separate.
Can a Temporal Signal authorize an automatic RelayLink reply?
No. A Signal can carry a user's recorded review decision into the Workflow, but an outbound RelayLink send still requires explicit approval through RelayLink's send flow.