Skip to main content
The shape: one workflow per unit of work. The workflow’s activities create the session, collect its answer and delete it; a small webhook receiver turns Gobare’s turn.completed into a Temporal signal that wakes the workflow. If that webhook never arrives, the workflow notices and polls instead. Verified with @temporalio/client, worker, workflow, activity and testing 1.24.0 on Node 24.

Why these pieces

Session creation is an activity, never workflow code. Workflow code must be deterministic, and an HTTP call is the opposite of that.

Activities

Every call to Gobare. Temporal’s retry policy — five attempts below — is the only retry loop; the activities themselves do not retry.
Never ask a clarifying question is in the instructions because this workflow has no one to ask. A session parked on a question reports its turn as working, so the fallback poll would keep waiting until the workspace’s two-hour ceiling. If your work needs a person, send the question somewhere a person is — Put an agent in a Slack thread — and signal the workflow with the answer.

The workflow

The signal handler keeps the first turn it is told about (??=), so a webhook delivered twice signals twice and changes nothing. A signal that arrives while the workflow is between activities is buffered by Temporal until the handler runs. fallbackAfter is an argument so a test can make it seconds. Fifteen minutes is a reasonable production default: long enough that a working webhook always wins, short enough that a lost one costs a quarter of an hour.

The webhook receiver

Verifies the delivery, finds the workflow through the session’s metadata, and signals it. It answers 200 for everything it does not act on — anything else is retried by Gobare for hours.
A turn event whose turn_id is null — there was no turn record to name — is not signalled; the workflow’s fallback poll finds the turn itself. A workflow that already finished — it polled first, or it was cancelled — answers the signal with WorkflowNotFoundError. That is a normal outcome, not a failure to retry. Subscribe the receiver’s URL once:

Running it

The worker, which runs the workflow code and the activities:
And the entry point your own system calls — a tracker webhook, a queue consumer, a button:
workflowId: ticket-${id} is Temporal’s half of the idempotency: a second start for the same ticket is refused with WorkflowExecutionAlreadyStartedError while the first is running, so a re-delivered ticket starts nothing.

What was verified

Run against production, with a local Temporal dev server (TestWorkflowEnvironment.createLocal() from @temporalio/testing) standing in for your cluster:
  • The webhook path. A workflow started, its session answered, a signed turn.completed was delivered over HTTP to the receiver above, the receiver signalled the workflow, and the workflow returned {"status":"completed","reply":"42"} and deleted the session. The delivery was signed with Gobare’s own signing function rather than a copy of it, because the receiver could not be reached from the internet.
  • The fallback path. With fallbackAfter at five seconds and no webhook at all, the workflow polled and finished completed with the correct answer.
  • Retries. During that run an activity’s connection to api.gobare.dev timed out after ten seconds; Temporal retried it and the workflow finished as if nothing had happened. That is the case this integration exists for.
  • Idempotency. A second start for a running workflow id was refused with WorkflowExecutionAlreadyStartedError; startSession called twice with one workflow id returned the same session both times.
Not verified: a Temporal Cloud namespace, mTLS, and a workflow that outlives a worker restart — the last is Temporal’s guarantee rather than ours, and nothing above depends on in-memory state.

Next