Skip to content

Repository files navigation

Firequeue

A durable workflow orchestrator for Firebase Cloud Functions.

Firequeue lets you write multi-step workflows that persist state to Firestore. Each step runs exactly once, and workflows can pause and resume across function invocations.

Table of Contents

Installation

# pnpm
pnpm add @fireq/firequeue
# npm
npm install @fireq/firequeue
# yarn
yarn add @fireq/firequeue

How It Works

  1. Invoke: Call firequeue.invokeTask() to create a task document in Firestore.
  2. Trigger: A Cloud Function created by firequeue.createTask() is triggered by the document write.
  3. Execute: The function runs through your workflow. Each step.run() call:
    • Creates a step document in a subcollection (if it doesn't exist)
    • Executes the step function and saves the result
    • In default mode: stops execution and re-triggers the function for the next step
    • In speculative mode: continues to the next step until timeout approaches
  4. Resume: On subsequent runs, completed steps return their saved result without re-executing.

Execution Modes

ModeBehavior
serializable (default)Executes one step per function invocation, then re-triggers
speculativeExecutes as many steps as possible until timeoutSeconds - 5s

Task Statuses

StatusDescription
scheduledTask is queued for execution
runningTask is currently executing
waitingTask is waiting for an event via step.waitForEvent()
completedAll steps finished successfully
cancelledTask was cancelled
errorA step failed

Step Statuses

StatusDescription
scheduledStep is ready to execute
runningStep is currently executing
completedStep finished and result is saved
cancelledStep was cancelled
errorStep execution failed

Usage

Initialization

// src/init.tsimport{createFirequeue}from"@fireq/firequeue";import*asadminfrom"firebase-admin";admin.initializeApp();// Define your task input typesinterfaceTaskRegistry{"order-processing": {orderId: string;customerId: string};"send-email": {to: string;subject: string};}exportconstfirequeue=createFirequeue<TaskRegistry>({firestore: admin.firestore(),logLevel: "debug",// optional});

Defining a Task

// src/functions.tsimport{firequeue}from"./init";exportconstprocessOrder=firequeue.createTask("order-processing",{collectionPath: "queue",timeoutSeconds: 120,executionMode: "speculative",// optional, defaults to "serializable"},async({ step, input, taskInstanceId })=>{// Step 1constpayment=awaitstep.run("process-payment",async()=>{returnprocessPayment(input.orderId);});// Step 2awaitstep.run("update-inventory",async()=>{returnupdateInventory(input.orderId);});// Wait for external event (e.g., webhook confirmation)awaitstep.waitForEvent({event: "payment-confirmed"});// Step 3awaitstep.run("send-confirmation",async()=>{returnsendEmail(input.customerId,payment);});});

Invoking a Task

import{firequeue}from"./init";awaitfirequeue.invokeTask({taskId: "order-processing",collectionPath: "queue",input: {orderId: "123",customerId: "456"},});

Sending Events

When a task is waiting for an event via step.waitForEvent(), send the event to resume execution:

awaitfirequeue.sendEvent({taskInstanceId: "abc123",collectionPath: "queue",event: "payment-confirmed",});

Cancelling Tasks

// Cancel an entire taskawaitfirequeue.cancelTask({taskInstanceId: "abc123",collectionPath: "queue",});// Cancel specific stepsawaitfirequeue.cancelSteps({taskInstanceId: "abc123",collectionPath: "queue",stepIds: ["step-1","step-2"],});

Retrying Failed Steps

Use invalidateTask to reschedule failed steps:

awaitfirequeue.invalidateTask({taskInstanceId: "abc123",collectionPath: "queue",stepIds: ["failed-step"],events: ["event-to-recreate"],// optional});

API Reference

createFirequeue(options)

Creates a Firequeue instance.

OptionTypeDescription
firestoreFirebaseFirestore.FirestoreFirestore instance from firebase-admin
serializerSerializerOptional. Custom serializer for step results. Default handles undefined, null, NaN
logLevelLogSeverityOptional. Log level (debug, info, warn, error)

firequeue.createTask(taskId, options, run)

Creates a Firestore-triggered Cloud Function.

Options:

OptionTypeDescription
collectionPathstringFirestore collection path for task documents
executionMode"serializable" | "speculative"Optional. Default: "serializable"
timeoutSecondsnumberOptional. Function timeout
concurrencynumberOptional. Max concurrent instances
secretsstring[]Optional. Secret names to expose

Run function parameters:

ParameterTypeDescription
stepStepFactoryStep execution utilities
inputT | nullInput data passed to invokeTask
taskInstanceIdstringUnique ID for this task instance
eventFirestoreEventThe Firestore trigger event

step.run(stepId, fn)

Executes a durable step.

ParameterTypeDescription
stepIdstringUnique step identifier within the task
fn() => Promise<T>Async function to execute. Return value is persisted

Returns the step result (from execution or cache).

step.waitForEvent(options)

Pauses task execution until an event is received.

OptionTypeDescription
eventstringEvent name to wait for
timeoutTimeStringOptional. Timeout (e.g., "5m", "1h")

firequeue.invokeTask(options)

Starts a new task execution.

OptionTypeDescription
taskIdstringTask identifier (must match createTask)
collectionPathstringCollection path (must match createTask)
inputTOptional. Input data for the task

firequeue.sendEvent(options)

Sends an event to a waiting task.

OptionTypeDescription
taskInstanceIdstringTask instance ID
collectionPathstringCollection path
eventstringEvent name

firequeue.cancelTask(options)

Cancels a task.

OptionTypeDescription
taskInstanceIdstringTask instance ID
collectionPathstringCollection path

firequeue.cancelSteps(options)

Cancels specific steps and sets task status to cancelled.

OptionTypeDescription
taskInstanceIdstringTask instance ID
collectionPathstringCollection path
stepIdsstring[]Step IDs to cancel

firequeue.invalidateTask(options)

Reschedules steps for retry and sets task status to scheduled.

OptionTypeDescription
taskInstanceIdstringTask instance ID
collectionPathstringCollection path
stepIdsstring[]Step IDs to reschedule
eventsstring[]Optional. Events to recreate

Gotchas

Speculative Execution Can Timeout

In speculative mode, multiple steps run in a single function invocation. If the workflow takes longer than timeoutSeconds, the function will timeout and the workflow will not automatically resume. The task will be left in running status.

Use speculative execution only for short workflows where you want the step structure (idempotency, result caching) but don't need the reliability of per-step re-invocation.

// Good: small workflow, steps are fastfirequeue.createTask("send-notification",{collectionPath: "queue",executionMode: "speculative",},async({ step })=>{constuser=awaitstep.run("get-user",()=>getUser());awaitstep.run("send-email",()=>sendEmail(user));});// Bad: long workflow with slow stepsfirequeue.createTask("process-video",{collectionPath: "queue",executionMode: "speculative",// Don't do this},async({ step })=>{awaitstep.run("download",()=>downloadVideo());// 30sawaitstep.run("transcode",()=>transcodeVideo());// 60sawaitstep.run("upload",()=>uploadVideo());// 30s});

Step IDs Must Be Unique Within a Task

Each step.run() call must have a unique stepId. If you use the same ID twice, the second call will return the cached result from the first execution.

// Wrong: both steps have the same IDawaitstep.run("process",()=>processA());awaitstep.run("process",()=>processB());// Returns result of processA()// Correctawaitstep.run("process-a",()=>processA());awaitstep.run("process-b",()=>processB());

Step Results Must Be Serializable

Step return values are stored in Firestore as JSON. Functions, symbols, circular references, and other non-serializable values will cause errors or be lost.

// Wrong: returning a functionawaitstep.run("bad",()=>{return{callback: ()=>{}};// Will fail or be lost});// Correct: return plain dataawaitstep.run("good",()=>{return{id: "123",name: "test"};});

If you need to serialize types that JSON doesn't support (e.g., Date, BigInt, custom classes), provide a custom serializer:

importsuperjsonfrom"superjson";constfirequeue=createFirequeue({firestore: admin.firestore(),serializer: {stringify: (data)=>superjson.stringify(data),parse: (str)=>superjson.parse(str),},});

Conditional Steps Need Unique IDs

If you have conditional logic, make sure step IDs are unique across all branches:

// Wrong: step ID collision between branchesif(condition){awaitstep.run("send",()=>sendEmail());}else{awaitstep.run("send",()=>sendSms());// ID collision!}// Correctif(condition){awaitstep.run("send-email",()=>sendEmail());}else{awaitstep.run("send-sms",()=>sendSms());}

Events Must Be Sent After Task Starts Waiting

If you send an event before the task reaches step.waitForEvent(), the event will be consumed immediately. If the task hasn't started yet or is still on earlier steps, make sure your event sender waits for the appropriate state.

Zombie Step Detection

If a step is in running status for longer than timeoutSeconds + 10s, it's marked as error (zombie detection). This handles cases where a function crashed mid-step without updating the status.

Firestore Structure

{collectionPath}/
{taskInstanceId}/ # Task document
steps/
{stepId}/ # Step documents
events/
{eventId}/ # Event documents

About

Firequeue is a durable workflow orchestrator for Firebase Cloud Functions, enabling long-running, reliable, multi-step workflows that can pause and resume. Inspired by Inngest, it offers a simple async/await API built on Cloud Functions and Firestore for defining complex, stateful processes resilient to timeouts and failures.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages