Skip to content

# Context Object

The context object is pgflow’s mechanism for providing handlers with access to platform resources like database connections, environment variables, and service clients. Instead of each handler creating its own connections, pgflow manages these resources centrally and injects them through a standardized context parameter.

Future: Custom Resources

The context system is designed to be extensible. Future versions are planned to support custom resources, allowing you to add your own services (Redis, Stripe, etc.) to the context object while maintaining full type safety.

## Why Context Exists

Section titled “Why Context Exists”

Before context, handlers faced several challenges:

1. Resource Management - Each handler needed to create its own database connections and clients
2. Configuration Access - No standardized way to access environment variables
3. Boilerplate Code - Repeated connection setup in every handler
4. Resource Leaks - Risk of not properly closing connections
5. Testing Complexity - Difficult to mock resources for testing

The context object solves these problems by providing pre-configured, managed resources that handlers can use immediately.

## Core Principles

Section titled “Core Principles”

Context follows pgflow’s design philosophy:

1. Zero Configuration - Resources are ready to use without setup
2. Type Safety - Full TypeScript support with proper inference
3. Pre-configured Resources - Platform adapter provides established connections
4. Platform Agnostic - Context interface can adapt to different platforms
5. Backward Compatible - Existing single-parameter handlers continue to work

## How Context Works

Section titled “How Context Works”

When pgflow executes a handler, it passes two parameters:

### Before vs After

Section titled “Before vs After”

- Before (Global Resources)
- After (Context)

    import { sql } from '../db.js';import { supabase } from '../supabase-client.js';
    async function processUser(input: { userId: string }) {  const [user] = await sql`SELECT * FROM users WHERE id = ${input.userId}`;  const apiKey = process.env.EXTERNAL_API_KEY;  const { data } = await supabase.auth.getUser();
      return { user };}

    async function processUser(input: { userId: string }, ctx: Context) {  const [user] = await ctx.sql`SELECT * FROM users WHERE id = ${input.userId}`;  const apiKey = ctx.env.EXTERNAL_API_KEY;  const { data } = await ctx.supabase.auth.getUser();
      return { user };}

### Complete Flow Example

Section titled “Complete Flow Example”
Using context in flows

    const ProcessUserFlow = new Flow<{ userId: string }>({  slug: 'process_user'})  .step({ slug: 'validate' }, async (input, ctx) => {    const [user] = await ctx.sql`SELECT * FROM users WHERE id = ${input.userId}`;    const apiKey = ctx.env.EXTERNAL_API_KEY;    const { data } = await ctx.supabase.auth.getUser();
        // ... process with resources    return { user };  });

Note

The context parameter is optional. Handlers that don’t need platform resources can omit it for backward compatibility.

## Core Resources

Section titled “Core Resources”

These resources are always available regardless of the platform adapter being used.

### `env`

Section titled “env”

Type: `Record<string, string | undefined>` Available: Always

Environment variables from the runtime environment. The exact variables depend on your platform and deployment configuration.

    // Queue handlerasync function handler(input, ctx) {  const apiKey = ctx.env.API_KEY;  const endpoint = ctx.env.SERVICE_ENDPOINT;}

### `shutdownSignal`

Section titled “shutdownSignal”

Type: `AbortSignal` Available: Always

An AbortSignal that triggers when the worker is shutting down. Use this to gracefully handle long-running operations and ensure clean shutdown.

    // Step handler.step({ slug: 'fetch_data' }, async (input, ctx) => {  // Automatically cancels if worker shuts down  const response = await fetch('https://api.example.com/data', {    signal: ctx.shutdownSignal,    method: 'POST',    body: JSON.stringify(input)  });
      return response.json();})

### `rawMessage`

Section titled “rawMessage”

Type: `PgmqMessageRecord<T>` Available: Always

The original message from the pgmq queue, containing metadata like message ID, read count, and enqueued timestamp. Useful for debugging and advanced queue operations.

    interface PgmqMessageRecord<T> {  msg_id: number;       // Unique message ID from pgmq  read_ct: number;      // How many times this message has been read  enqueued_at: string;  // ISO timestamp when message was enqueued  vt: string;           // ISO timestamp for visibility timeout  message: T;           // The actual payload}

    async function handler(input, ctx) {  console.log(`Processing message ${ctx.rawMessage.msg_id}`);  console.log(`Attempt ${ctx.rawMessage.read_ct} of this message`);  console.log(`Enqueued at ${ctx.rawMessage.enqueued_at}`);}

### `stepTask`

Section titled “stepTask”

Type: `StepTaskRecord<TFlow>` Available: Flow handlers only

Details about the current step task being executed. This is a strongly-typed record that provides the essential task identification along with the typed input for the specific step. Only available in flow step handlers, not in queue workers.

    interface StepTaskRecord<TFlow> {  flow_slug: string;    // Slug identifier of the flow  run_id: string;       // UUID of the current flow run  step_slug: string;    // Slug identifier of the current step  task_index: number;   // Task index (0 for single steps, 0..N-1 for map steps)  input: StepInput;     // Typed input for this specific step (inferred from flow)  msg_id: number;       // pgmq message ID}

    const MyFlow = new Flow({ slug: 'my_flow' })  .step({ slug: 'process' }, async (input, ctx) => {    console.log(`Executing step: ${ctx.stepTask.step_slug}`);    console.log(`For run: ${ctx.stepTask.run_id}`);    console.log(`Flow: ${ctx.stepTask.flow_slug}`);    console.log(`Task index: ${ctx.stepTask.task_index}`); // 0 for single steps    console.log(`Message ID: ${ctx.stepTask.msg_id}`);    // ctx.stepTask.input is the same as the input parameter  })  .map({ slug: 'processItems', array: 'items' }, async (item, ctx) => {    // For map steps, task_index indicates which array element    console.log(`Processing item ${ctx.stepTask.task_index} of array`);    return processItem(item);  });

### `workerConfig`

Section titled “workerConfig”

Type: `Readonly<ResolvedQueueWorkerConfig>` or `Readonly<ResolvedFlowWorkerConfig>` Available: Always

The resolved worker configuration for the current worker instance. This is immutable and provides access to all worker settings including retry policies, concurrency limits, and timeouts. The configuration excludes the SQL connection object since it cannot be safely cloned and frozen.

All optional configuration values have been resolved with their defaults applied, so you can safely access any field without checking for `undefined`.

    // Resolved queue worker configuration (all defaults applied)interface ResolvedQueueWorkerConfig {  queueName: string;          // Name of the queue being processed  maxConcurrent: number;      // Maximum concurrent tasks  retry: RetryConfig;         // Retry strategy configuration  visibilityTimeout: number;  // Message visibility timeout in seconds  batchSize: number;          // Batch size for polling messages  maxPollSeconds: number;     // Maximum polling duration  pollIntervalMs: number;     // Polling interval in milliseconds  maxPgConnections: number;   // Database connection pool size  connectionString: string;   // Database connection string (may be empty)  env: Record<string, string | undefined>;  // Environment variables}
    // Resolved flow worker configuration (all defaults applied)interface ResolvedFlowWorkerConfig {  maxConcurrent: number;      // Maximum concurrent tasks  batchSize: number;          // Batch size for polling messages  visibilityTimeout: number;  // Message visibility timeout in seconds  maxPollSeconds: number;     // Maximum polling duration  pollIntervalMs: number;     // Polling interval in milliseconds  maxPgConnections: number;   // Database connection pool size  connectionString?: string;  // Database connection string (optional)  env: Record<string, string | undefined>;  // Environment variables}

The most common use case is detecting when a message is on its final retry attempt:

    async function sendEmail(input, ctx) {  const isLastAttempt = ctx.rawMessage.read_ct >= ctx.workerConfig.retry.limit;
      if (isLastAttempt) {    // Use fallback email service on final attempt    return await sendWithFallbackProvider(input.to, input.subject, input.body);  }
      // Use primary email service for regular attempts  return await sendWithPrimaryProvider(input.to, input.subject, input.body);}

Note

The `workerConfig` object is deeply frozen to prevent handlers from modifying configuration values that could affect other message executions. It represents the final resolved configuration with all defaults applied, not the original user input. Since all defaults have been resolved, you can safely access any field without undefined checks.

## Supabase Resources

Section titled “Supabase Resources”

These resources are available when using the Supabase platform adapter.

### `sql`

Section titled “sql”

Type: `postgres.Sql` Available: Supabase platform

A configured PostgreSQL client from the postgres.js library, ready for executing SQL queries against your database.

    // Queue handlerasync function handler(input, ctx) {  const users = await ctx.sql`    SELECT * FROM users    WHERE created_at > ${input.since}  `;  return { userCount: users.length };}

### `supabase`

Section titled “supabase”

Type: `SupabaseClient` Available: Supabase platform

Supabase client authenticated with the service role key. This has full database access and bypasses RLS. Since Edge Functions run in a trusted environment, this is the only client you need.

    // Step handler.step({ slug: 'process_order' }, async (input, ctx) => {  const { data, error } = await ctx.supabase    .from('orders')    .update({ status: 'processing' })    .eq('id', input.orderId);
      return { order: data };})

    // Queue handlerasync function handler(input, ctx) {  const { data, error } = await ctx.supabase    .from('users')    .update({ verified: true })    .eq('id', input.userId);}

## Using Destructuring for Cleaner Code

Section titled “Using Destructuring for Cleaner Code”

While examples in this documentation use `ctx.` for clarity, destructuring often makes your code more readable:

    // Queue handler - destructure only what you needasync function processOrder(input, { sql, env, rawMessage }) {  console.log(`Processing order ${rawMessage.msg_id}`);
      const [order] = await sql`    SELECT * FROM orders WHERE id = ${input.orderId}  `;
      await sendNotification(order, env.NOTIFICATION_API_KEY);}

    // Step handler - clean and focused.step({ slug: 'sync_data' }, async (input, { sql, supabase, stepTask }) => {  console.log(`Syncing for run ${stepTask.run_id}`);
      const data = await sql`SELECT * FROM pending_sync WHERE user_id = ${input.userId}`;
      const synced = await supabase    .from('synced_data')    .upsert(data)    .select();
      return { syncedCount: synced.data.length };})

Tip

Destructuring makes it immediately clear which resources a handler depends on, improving code readability and testability.

## Summary

Section titled “Summary”

The context object streamlines handler development by:

- Providing ready-to-use platform resources
- Eliminating connection boilerplate
- Centralizing resource access through platform adapter
- Maintaining type safety throughout
- Supporting gradual migration of existing code

Context embodies pgflow’s philosophy of being robust yet simple - handlers get powerful capabilities without complexity.