Skip to content

Configuration

Everything is configured through AddPgWorkflows(pg => ...) on your service collection.

builder.Services.AddPgWorkflows(pg =>
pg.UsePostgres(connectionString)
.ConfigureWorkflowWorker(options => options with { /* ... */ })
.ConfigureActivityWorker(options => options with { /* ... */ })
.AddWorkflow<TrialOnboardingWorkflow>()
.AddActivities<EmailActivities>()
);
MethodPurpose
UsePostgres(connectionString, ensureSchemaOnStart = true, maxPoolSize = null)Point PgWorkflows at your database. Creates the schema on startup by default and sizes the connection pool to the worker concurrency unless told otherwise (see connection pooling).
DisableWorkers()Client-only mode: the process can start, signal, and await workflows but runs no background workers. See workers & scaling.
ConfigureWorkflowWorker(options => ...)Tune the workflow worker (see below).
ConfigureActivityWorker(options => ...)Tune the activity worker (see below).
AddWorkflow<TWorkflow>()Register a workflow class.
AddActivities<TActivities>()Register an activity class (all [Activity] methods).
RegisterActivity<TInput, TOutput>(...)Register a single delegate-based activity.

UsePostgres also accepts a pre-built NpgsqlDataSource instead of a connection string, for apps that already manage their own data source; its pool settings are used as-is and validated against the worker concurrency.

RegisterActivity is the low-level escape hatch next to AddActivities: it binds a delegate to an explicit durable name. Overloads accept sync or async delegates, an optional CancellationToken, and an optional ActivityExecutionContext first parameter carrying the job id and attempt number.

The schema is versioned. On startup (with ensureSchemaOnStart: true, the default), PgWorkflows checks the pw_schema_migrations table and applies only the migrations your database is missing, in order, inside a transaction guarded by an advisory lock. The lock serializes a fleet starting at once so only one process applies pending DDL while the rest wait.

Automatic migration is convenient for development and straightforward upgrades. For critical or rolling deployments, review migration compatibility and apply migrations out of band before new workers start. Schema migration does not make incompatible workflow-code changes safe; see deterministic workflow code.

To apply migrations out-of-band instead (a deploy pipeline, a DBA), pass ensureSchemaOnStart: false and use PostgresSchema.Migrations: execute each pending migration’s Sql and insert its row into pw_schema_migrations(version, name, applied_at) in the same transaction.

Workers rent connections for individual store operations rather than holding one for the whole activity or workflow pass. Many in-flight items can perform database operations concurrently, so UsePostgres sizes the pool from ActivityWorker.MaxConcurrency + WorkflowWorker.MaxConcurrency plus headroom (a client-only process gets 20). A single shared heartbeat renews all of a worker’s leases in one query rather than opening a renewal operation per item.

Override the per-process cap explicitly with the maxPoolSize parameter (e.g. to share a budget across a large fleet); an explicit pool that is too small for the configured concurrency fails fast at startup:

pg.UsePostgres(connectionString, maxPoolSize: 40)

Or in the connection string, which is respected when maxPoolSize is not passed:

Host=db;Database=app;Username=app;Maximum Pool Size=40

If you pass a pre-built NpgsqlDataSource to UsePostgres, its pool settings are used as-is (and validated against the concurrency, failing fast if too small).

When you do set the pool explicitly, budget across the whole fleet: every API and worker process holds its own pool, and the sum of all pools must stay below the server’s max_connections (Npgsql’s and Postgres’ default is 100) with room to spare for migrations, dashboards, and ad-hoc connections.

OptionDefaultDescription
WorkerIdmachine nameIdentifies this worker in leases.
BatchSize16Runs leased per database round-trip, capped at MaxConcurrency. A round-trip amortization knob, not a concurrency limit; the worker refills slots continuously.
MaxConcurrency10Max runs in flight at once. The worker keeps this many running, leasing a replacement the moment one finishes. The connection pool sizes itself to this (plus the activity worker’s share); see connection pooling.
LeaseDuration30 sHow long a lease lives between heartbeats.
PollInterval250 msHow often an idle worker polls for work.
ParkGrace30 sSafety-net deadline for runs parked on activity steps; they’re normally woken sooner by the edge-trigger.
MaxAttempts1Workflow-level attempts. Transient database errors do not consume an attempt; see error handling.
GetRetryDelaymin(attempt × 5 s, 60 s)Backoff between attempts. Also used as the delay before a run released by a transient database error becomes visible again.
OptionDefaultDescription
WorkerIdmachine nameIdentifies this worker in leases.
BatchSize16Jobs leased per database round-trip, capped at MaxConcurrency. A round-trip amortization knob, not a concurrency limit; the worker refills slots continuously.
MaxConcurrency10Max activities in flight at once, refilled the moment one finishes. The connection pool sizes itself to this (plus the workflow worker’s share); see connection pooling.
LeaseDuration30 sHow long a lease lives between heartbeats.
PollInterval250 msHow often an idle worker polls for work.
GetRetryDelaymin(attempt × 5 s, 60 s)Backoff between activity retry attempts.

There is no per-activity retry budget yet: an activity called from a workflow gets a single execution attempt, and its failure surfaces to the workflow (see error handling). Configurable activity retries are planned.