Inngest

Docs › Steps

step.waitForEvent

step.waitForEvent() pauses a run until a matching event arrives or the wait times out. Inngest resumes the run with the event without keeping your function process running while it waits.

They are useful when:

  • You need a response from another service before continuing.
  • A person must approve or reject work.
  • One event should be able to resume multiple matching runs.

Why choose an event?

  • Resume several runs. One event can resume multiple matching waits and trigger other functions.
  • Keep systems independent. The sender publishes an event; it does not need to know which runs are waiting.
  • Keep an audit trail. Inngest stores events for audit and Insights.

Use step.waitForEvent for most waits. If you must address one run directly and need lower resume latency, use step.waitForSignal. A signal resumes one run through a transactional API.

Wait for invoice review

This function waits up to seven days for an invoice review. It matches the review to the invoice that started the run.

TypeScript
import { eventType, Inngest, staticSchema } from "inngest";

type ApprovalRequested = { invoiceId: string };
type ApprovalRecorded = { invoiceId: string; approved: boolean };

const approvalRequested = eventType("invoice/approval.requested", {
  schema: staticSchema<ApprovalRequested>(),
});
const approvalRecorded = eventType("invoice/approval.recorded", {
  schema: staticSchema<ApprovalRecorded>(),
});

const inngest = new Inngest({ id: "billing" });

export const awaitInvoiceReview = inngest.createFunction(
  { id: "await-invoice-review", triggers: [approvalRequested] },
  async ({ event, step }) => {
    const review = await step.waitForEvent("wait-for-review", {
      event: approvalRecorded,
      match: "data.invoiceId",
      timeout: "7d",
    });

    if (review === null) {
      return { invoiceId: event.data.invoiceId, status: "timed-out" };
    }

    return {
      invoiceId: event.data.invoiceId,
      status: review.data.approved ? "approved" : "rejected",
    };
  }
);
Python
import datetime

import inngest

inngest_client = inngest.Inngest(app_id="billing")

@inngest_client.create_function(
    fn_id="await-invoice-review",
    trigger=inngest.TriggerEvent(event="invoice/approval.requested"),
)
async def await_invoice_review(ctx: inngest.Context) -> dict[str, str]:
    invoice_id = str(ctx.event.data["invoiceId"])

    review = await ctx.step.wait_for_event(
        "wait-for-review",
        event="invoice/approval.recorded",
        if_exp="event.data.invoiceId == async.data.invoiceId",
        timeout=datetime.timedelta(days=7),
    )

    if review is None:
        return {"invoiceId": invoice_id, "status": "timed-out"}

    return {
        "invoiceId": invoice_id,
        "status": "approved" if review.data["approved"] else "rejected",
    }
Go
main.go
import (
	"context"
	"errors"
	"time"

	"github.com/inngest/inngestgo"
	"github.com/inngest/inngestgo/step"
)

type ApprovalRequested struct {
	InvoiceID string `json:"invoiceId"`
}

type ApprovalRecorded struct {
	InvoiceID string `json:"invoiceId"`
	Approved  bool   `json:"approved"`
}

func main() {
	client, err := inngestgo.NewClient(inngestgo.ClientOpts{AppID: "billing"})
	if err != nil {
		panic(err)
	}

	_, err = inngestgo.CreateFunction(
		client,
		inngestgo.FunctionOpts{ID: "await-invoice-review"},
		inngestgo.EventTrigger("invoice/approval.requested", nil),
		func(ctx context.Context, input inngestgo.Input[ApprovalRequested]) (any, error) {
			review, err := step.WaitForEvent[inngestgo.GenericEvent[ApprovalRecorded]](
				ctx,
				"wait-for-review",
				step.WaitForEventOpts{
					Event:   "invoice/approval.recorded",
					If:      inngestgo.StrPtr("event.data.invoiceId == async.data.invoiceId"),
					Timeout: 7 * 24 * time.Hour,
				},
			)
			if errors.Is(err, step.ErrEventNotReceived) {
				return map[string]string{
					"invoiceId": input.Event.Data.InvoiceID,
					"status":    "timed-out",
				}, nil
			}
			if err != nil {
				return nil, err
			}

			status := "rejected"
			if review.Data.Approved {
				status = "approved"
			}
			return map[string]string{
				"invoiceId": input.Event.Data.InvoiceID,
				"status":    status,
			}, nil
		},
	)
	if err != nil {
		panic(err)
	}
}

Send invoice/approval.recorded with the same invoiceId after the wait starts. The returned event provides review.data.approved; a timeout returns null.

API

TypeScript
const event = await step.waitForEvent(id, { event, timeout, match });
Python
event = await ctx.step.wait_for_event(
    step_id, event=name, timeout=timeout, if_exp=expression
)
Go
event, err := step.WaitForEvent[inngestgo.Event](ctx, id, step.WaitForEventOpts{
	Event:   name,
	Timeout: timeout,
	If:      &expression,
})
  • id: a stable step ID.
  • event: the name of the event to wait for.
  • timeout: how long to wait, such as "7d".
  • match: a field both events share, such as "data.invoiceId". For more specific matches, use an if expression instead.
  • Returns the matching event, or null if the timeout expires.

Always handle the timeout, for example by recording an expired request or sending a reminder. See the step.waitForEvent reference for every option, including if expressions.

Match the right event

Use a stable request or entity ID in both events. An event name alone can match unrelated runs. Sessions do not filter step.waitForEvent(); the event name and match or if determine the match.

The wait only sees events sent after it starts listening. If an external system can send a response before the wait begins, arrange the sequence so the wait is registered first or check the external state before waiting. A matching event sent earlier will not resume this wait.

Use step.waitForEvent() for events that can fan out to multiple functions or runs. For direct, latency-sensitive resumption of one run, see step.waitForSignal.

  • Check the current platform limits when choosing a timeout and shaping event or run data. The limit for a running step is a separate setting from a durable wait.
  • In a group.parallel() race, a losing step.waitForEvent() remains active until its timeout. Use a short timeout when that pattern applies.
  • For a delay with no external event, use step.sleep.

Updated Nov 10, 2026

Powered byZunoy