@pyck/workflow-sdk
v0.4.0
Published
Pyck Workflow SDK — TypeScript primitives for building Temporal workflows.
Keywords
Readme
@pyck/workflow-sdk
TypeScript primitives for building Temporal workflows on the Pyck platform.
The SDK gives you a thin, opinionated layer over the Temporal SDK: a registry for wiring workflows and activities, a setup → runDefaultWorker lifecycle, NATS-backed signal bindings registered with the Pyck gateway, typed user-input updates, workflow search-attribute helpers (title, assignee, targets, grouping, sort key), and GraphQL/HTTP clients for talking to Pyck services.
Status: Alpha. APIs may change between minor versions.
Features
- Worker lifecycle —
setup()to register at module load,runDefaultWorker()to connect, sync with the Pyck gateway, and block until shutdown. - Registry — declare workflows and activities; task queues are derived automatically.
- Worker Deployment Versioning — pinned deployment versions so rolling deploys don't break in-flight executions, with UI bundle metadata stamped on the version.
- Health & readiness —
/health(is this worker still polling Temporal?) and/ready(is its bundle metadata stamped?) for fly.io, Kubernetes, or any supervisor. - Subscription heartbeat — periodic re-registration keeps this worker's signal subscriptions alive past their TTL, with conflict-aware retries at startup.
- Signals — bind NATS topics to Temporal workflows as
startorintermediatesignals, with optional filter rules. - Updates —
WorkflowUpdatebase class that pauses a workflow until typed, schema-validated user input arrives. - Search-attribute helpers — set/get workflow title, assignee, targets, group-by, group title, and sort key.
- Clients — GraphQL and HTTP clients plus query/update helpers for the Pyck Workflow API.
- Config from env — one
loadEnv()call populates a process-wideConfig.
Installation
# bun
bun add @pyck/workflow-sdk
# npm
npm install @pyck/workflow-sdk
# pnpm
pnpm add @pyck/workflow-sdkPeer dependencies
Install the Temporal packages your runtime needs (peers, not bundled):
npm install \
@temporalio/activity@^1.15.0 \
@temporalio/client@^1.15.0 \
@temporalio/envconfig@^1.15.0 \
@temporalio/worker@^1.15.0 \
@temporalio/workflow@^1.15.0 \
graphql@^16Requirements: Node.js 24+, TypeScript 5.6+.
Quick start
import { setup, runDefaultWorker } from '@pyck/workflow-sdk'
import { newStartSignal, MutationEventTopic } from '@pyck/workflow-sdk'
import { pickOrder } from './workflows'
import * as activities from './activities'
// Register at module load.
setup((registry) => {
registry.registerWorkflow({
workflow: pickOrder,
taskQueue: 'picking',
signals: [newStartSignal(new MutationEventTopic({ serviceName: 'orders', operationName: 'create' }))],
})
registry.registerActivities('picking', activities)
})
// Connect to Temporal, sync workflows with the Pyck gateway, run until SIGINT/SIGTERM.
await runDefaultWorker({
bundleOptions: { workflowsPath: './src/workflows' },
})Configuration
Configuration is read from the process environment. Call loadEnv() once at startup (runDefaultWorker does this for you), then read from the Config singleton.
import { Config, loadEnv } from '@pyck/workflow-sdk'
await loadEnv()
console.log(Config.environmentName, Config.gatewayUrl)Environment variables
| Variable | Default | Purpose |
|---|---|---|
| TEMPORAL_ADDRESS / TEMPORAL_HOST_URL | localhost:7233 | Temporal frontend address (:7233 appended if no port). |
| TEMPORAL_NAMESPACE | default | Temporal namespace. |
| TEMPORAL_TLS | false | Set true to enable TLS. |
| TEMPORAL_API_KEY | — | Temporal Cloud API key. |
| TEMPORAL_WORKFLOWS_PATH | — | Path to workflow code for bundling (or pass bundleOptions.workflowsPath). |
| PYCK_ENVIRONMENT_NAME | development | Logical environment name. |
| PYCK_GATEWAY_URL | — | Pyck gateway base URL. |
| PYCK_API_TOKEN | — | Token for GraphQL/HTTP clients. |
| PYCK_API_TENANT_ID | — | Tenant id for GraphQL/HTTP clients. |
| PYCK_LOG_LEVEL | info | Log level. |
| PYCK_LOG_FORMAT | json | Log format (json / text). |
| PYCK_WORKFLOW_MOCKING | false | Short-circuit activities with mock results. |
Gateway registration
| Variable | Default | Purpose |
|---|---|---|
| PYCK_WORKER_REGISTRATION_HEARTBEAT_INTERVAL | 5m | How often the worker refreshes its subscription TTLs. Must be shorter than the service's subscription TTL; non-positive disables the heartbeat. |
| PYCK_WORKER_REGISTRATION_RETRY_ATTEMPTS | 5 | Attempt budget for the initial registration on a transient conflict. |
| PYCK_WORKER_REGISTRATION_RETRY_BACKOFF | 1s | Base delay for the exponential backoff between registration retries. |
Worker Deployment Versioning
| Variable | Default | Purpose |
|---|---|---|
| PYCK_WORKER_BUILD_ID | — | Explicit build id, outranking everything else. Leave unset under the temporal-worker-controller. |
| PYCK_WORKER_DEPLOYMENT_NAME | — | Explicit deployment name. Same caveat. |
| TEMPORAL_WORKER_BUILD_ID | — | Build id injected by the controller; authoritative there. |
| TEMPORAL_DEPLOYMENT_NAME | — | Deployment name injected by the controller. |
| PYCK_WORKER_MODULE_VERSION | — | Version this build reports for itself when nothing above supplies one. Unset keeps the worker unversioned, so local dev runs out of the box. |
| PYCK_WORKER_MODULE_PATH | — | Module path whose basename is the fallback deployment name (else worker). |
| PYCK_WORKER_REQUIRE_BUILD_ID | false | Refuse to start on an unversioned build. Deploys set this true. |
| PYCK_WORKER_PROMOTE_ON_START | false | Promote this worker's version to current on start. Must stay false under the controller, which owns promotion. |
| PYCK_UI_BUNDLE_SLUG | — | Bundle slug stamped for every workflow type this worker serves. Empty for per-tenant bundles. |
| PYCK_UI_BUNDLE_VERSION | — | Bundle version stamped on the deployment version. Unset stamps nothing, so the backend serves its configured default. |
Health server
| Variable | Default | Purpose |
|---|---|---|
| HEALTH_DISABLED | — | Set 1 to skip binding the listener. |
| HEALTH_PORT | 8080 | Port for /health and /ready. |
| HEALTH_INTERVAL | 10s | Delay between probes. |
| HEALTH_TIMEOUT | 3s | Per-RPC deadline inside a probe. |
| HEALTH_MAX_STALE | 90s | How old the last successful probe may be before /health fails. |
Core concepts
Registry & setup
setup() collects registration callbacks invoked when a worker starts. Each callback receives a Registry:
setup((registry) => {
registry.registerWorkflow({ workflow: pickOrder, taskQueue: 'picking' })
registry.registerActivities('picking', activities)
})namedefaults to the workflow function's name.taskQueuedefaults to the lowercased name.- The worker spins up one Temporal worker per distinct task queue.
Worker
runDefaultWorker(options?)— full lifecycle: load env, resolve deployment versioning, connect, sync workflows with the gateway, stamp UI bundle metadata, serve/health+/ready, install SIGINT/SIGTERM handlers, run until stopped.newWorker(options?, logger?)— build a worker without auto-running, binding a health port, or installing signal handlers (for tests or multi-tenant hosts).
const w = await newWorker({ namespace: 'pyck' })
const running = w.run()
console.log(w.identity, w.taskQueues, w.deployment.versioned)
// ... later
await w.stop()
await runningEach worker pins a stable <pid>@<hostname> identity. The gateway scopes this
worker's signal subscriptions to it, and the health probe matches on it — so one
machine's wedged worker is never masked by another machine still polling the
same shared task queue.
Health & readiness
runDefaultWorker binds an HTTP listener (port 8080 by default) unless
HEALTH_DISABLED=1 or healthServer: false:
/health— 200 while a probe has recently confirmed that this worker's identity is a live poller on every task queue it serves. A worker whose poll loop wedged drops out of the poller set, so the check fails and the supervisor restarts the machine. The listener binds even when Temporal is unreachable and serves 503 with the dial error, rather than leaving the port unbound./ready— 200 once the worker is live and, for a versioned worker, its UI bundle metadata has been stamped. The temporal-worker-controller gates version promotion on readiness, so no version serves before its metadata exists.
await runDefaultWorker({ healthServer: { port: 9000, maxStaleMs: 60_000 } })
await runDefaultWorker({ healthServer: false }) // short-lived / test workersWorker Deployment Versioning
When a build id resolves (see the env table above), the worker registers a
Temporal Worker Deployment Version with PINNED behaviour: an execution stays
on the version it started on, so a rolling deploy cannot strand in-flight
workflows. The worker then stamps
ui.bundle.<WorkflowType>.{version,slug} metadata on that version, retrying
until it lands, so the backend can resolve the right remote UI bundle per
workflow type.
Without a build id the worker stays unversioned — which is what local dev wants.
Set PYCK_WORKER_REQUIRE_BUILD_ID=true in deploys so a build that expected
versioning fails loudly instead of silently running without it.
Signals
Signals bind a NATS topic to a workflow. A start signal launches a new workflow; an intermediate signal is delivered to a running one.
import {
newStartSignal,
newIntermediateSignal,
withFilterRule,
MutationEventTopic,
} from '@pyck/workflow-sdk'
const topic = new MutationEventTopic({ serviceName: 'orders', operationName: 'create' })
const start = newStartSignal(topic, withFilterRule('event.payload.status == "ready"'))
const cancel = newIntermediateSignal(topic, 'cancelOrder')Topic builders (MutationEventTopic, MutationEventWithReplyTopic) collapse empty fields to * wildcards, so a signal can match any tenant, entity, or operation.
Updates (user input)
WorkflowUpdate pauses a workflow until typed user input arrives, optionally validated against a TypeBox schema.
import { WorkflowUpdate } from '@pyck/workflow-sdk'
import { Type } from '@sinclair/typebox'
class ConfirmPick extends WorkflowUpdate<{ qty: number }, MyState> {
readonly typeId = 'confirm-pick'
override jsonSchema() {
return Type.Object({ qty: Type.Number() })
}
}
const confirm = new ConfirmPick()
await confirm.await(state.userDataInput, state)
console.log(confirm.value().qty)Reject malformed input from a validator with validationError('reason'), which throws a non-retryable ValidationError application failure.
Workflow search-attribute helpers
Set and read Pyck search attributes from inside a workflow:
import {
setWorkflowTitle, getWorkflowTitle,
setWorkflowAssignee, getWorkflowAssignee,
setWorkflowTargets, getWorkflowTargets,
setGroupBy, getGroupBy,
setWorkflowGroupTitle, getWorkflowGroupTitle,
setWorkflowSortKey, getWorkflowSortKey,
} from '@pyck/workflow-sdk'
setWorkflowTitle('Pick order #42')
setWorkflowSortKey(1747137600)Clients
Query a running workflow's pending user input, or use the GraphQL client for the Pyck Workflow API:
import { queryUserDataInput } from '@pyck/workflow-sdk/client'
import { createGraphQLClient } from '@pyck/workflow-sdk/graphql'
const input = await queryUserDataInput(handle)
const gql = createGraphQLClient({ /* token, tenantId, url */ })Entry points
The package exposes subpath exports for tree-friendly imports:
| Import | Contents |
|---|---|
| @pyck/workflow-sdk | Everything (setup, config, signals, updates, workflow helpers). |
| @pyck/workflow-sdk/worker | runDefaultWorker, newWorker, worker options, versioning, health server. |
| @pyck/workflow-sdk/workflow | Search-attribute helpers, user-data input, targets. |
| @pyck/workflow-sdk/signal | Signal builders and topics. |
| @pyck/workflow-sdk/registry | Registry, workflow/activity registries. |
| @pyck/workflow-sdk/client | Query/update client helpers. |
| @pyck/workflow-sdk/graphql | GraphQL client and types. |
Development
bun install
bun run build # tsc --build
bun run check # biome check --write
bun run lint # biome lint
bun run format # biome format --writeLicense
MIT © Pyck
