Temporal Reference: Workflows, Activities, Signals, TypeScript SDK & Production Patterns
Temporal is a durable execution platform — it makes long-running, failure-prone workflows look like ordinary code. Your workflow function can sleep for days, retry failed activities automatically, and survive server restarts mid-execution. The mental model: workflow code is a deterministic state machine replayed from history; activity code is the side-effectful work (API calls, DB writes, file I/O). The key constraint is workflow determinism — no random numbers, no direct API calls, no time.Now() in workflow code.
1. Core Concepts — Workflows & Activities (Go)
Define workflow and activity functions, register with worker, and start execution
package app
import (
"context"
"time"
"go.temporal.io/sdk/activity"
"go.temporal.io/sdk/workflow"
)
// Activity: the side-effectful work (API calls, DB writes, etc.)
// Activities can fail and be retried — all I/O goes here.
func SendEmailActivity(ctx context.Context, to string, subject string) error {
logger := activity.GetLogger(ctx)
logger.Info("Sending email", "to", to)
// Any I/O here — it will be retried on failure
return emailClient.Send(to, subject, "...")
}
func ChargePaymentActivity(ctx context.Context, userID string, amount float64) (string, error) {
// Returns a transaction ID
return stripeClient.Charge(userID, amount)
}
// Workflow: the orchestration logic — MUST be deterministic
// No: time.Now(), rand, direct HTTP calls, goroutines (use workflow.Go), maps with ranging
func OrderWorkflow(ctx workflow.Context, order Order) error {
logger := workflow.GetLogger(ctx)
// Activity options: retry policy, timeouts:
ao := workflow.ActivityOptions{
StartToCloseTimeout: 10 * time.Second,
RetryPolicy: &temporal.RetryPolicy{
InitialInterval: time.Second,
BackoffCoefficient: 2.0,
MaximumInterval: 100 * time.Second,
MaximumAttempts: 5,
},
}
ctx = workflow.WithActivityOptions(ctx, ao)
// Execute activities — blocks until complete or error:
var txID string
if err := workflow.ExecuteActivity(ctx, ChargePaymentActivity, order.UserID, order.Total).Get(ctx, &txID); err != nil {
return err
}
// workflow.Sleep is safe — survives restarts, doesn't block a thread:
_ = workflow.Sleep(ctx, 5*time.Minute)
if err := workflow.ExecuteActivity(ctx, SendEmailActivity, order.Email, "Order confirmed").Get(ctx, nil); err != nil {
logger.Warn("Email failed, continuing", "error", err)
}
return nil
}
2. Worker Setup & Starting Workflows
Connect worker to Temporal server, register workflows/activities, start + query executions
package main
import (
"go.temporal.io/sdk/client"
"go.temporal.io/sdk/worker"
)
func main() {
// Connect to Temporal server (local or Temporal Cloud):
c, err := client.Dial(client.Options{
HostPort: "localhost:7233", // or "your-namespace.tmprl.cloud:7233"
Namespace: "default",
})
defer c.Close()
// Worker: polls for tasks on this task queue:
w := worker.New(c, "order-task-queue", worker.Options{})
w.RegisterWorkflow(OrderWorkflow)
w.RegisterActivity(ChargePaymentActivity)
w.RegisterActivity(SendEmailActivity)
w.Run(worker.InterruptCh())
}
// Start a workflow (from your API server / anywhere):
we, err := c.ExecuteWorkflow(ctx, client.StartWorkflowOptions{
ID: "order-" + orderID, // idempotent key — re-running returns existing execution
TaskQueue: "order-task-queue",
WorkflowExecutionTimeout: 24 * time.Hour,
}, OrderWorkflow, order)
workflowID := we.GetID()
runID := we.GetRunID()
// Wait for result:
var result string
err = we.Get(ctx, &result)
// Query running workflow state:
resp, _ := c.QueryWorkflow(ctx, workflowID, runID, "getStatus")
// Signal a running workflow (external event):
c.SignalWorkflow(ctx, workflowID, runID, "paymentReceived", paymentPayload)
3. Signals, Queries & Child Workflows
Receive signals in workflow code, expose queryable state, and spawn child workflows
// Signals: receive external events in a running workflow
func SubscriptionWorkflow(ctx workflow.Context, userID string) error {
// Register query handler (returns current state to callers):
status := "active"
workflow.SetQueryHandler(ctx, "getStatus", func() (string, error) {
return status, nil
})
// Receive signal:
cancelCh := workflow.GetSignalChannel(ctx, "cancel")
renewCh := workflow.GetSignalChannel(ctx, "renew")
for {
// workflow.Sleep survives server restarts:
_ = workflow.Sleep(ctx, 30*24*time.Hour) // 30 days
// Check for signals (non-blocking selector):
selector := workflow.NewSelector(ctx)
selector.AddReceive(cancelCh, func(c workflow.ReceiveChannel, _ bool) {
status = "cancelled"
})
selector.AddReceive(renewCh, func(c workflow.ReceiveChannel, _ bool) {
status = "renewed"
})
if selector.HasPending() {
selector.Select(ctx)
}
if status == "cancelled" {
return nil
}
// Bill the user for another month:
ao := workflow.WithActivityOptions(ctx, workflow.ActivityOptions{
StartToCloseTimeout: 30 * time.Second,
})
workflow.ExecuteActivity(ao, BillUserActivity, userID).Get(ctx, nil)
}
}
// Child workflows — split long workflows or fan out parallel work:
cwo := workflow.ChildWorkflowOptions{
WorkflowID: "fulfillment-" + orderID,
TaskQueue: "fulfillment-queue", // can run on a different worker pool
}
childCtx := workflow.WithChildOptions(ctx, cwo)
workflow.ExecuteChildWorkflow(childCtx, FulfillmentWorkflow, order).Get(ctx, nil)
4. TypeScript SDK
Temporal TypeScript SDK — workflow/activity separation, proxies, and error handling
// activities.ts — I/O goes here (NOT in workflow code):
import { Context } from "@temporalio/activity";
export async function sendEmail(to: string, subject: string): Promise {
const { info } = Context.current();
console.log(`Attempt ${info.attempt}: sending to ${to}`);
await emailClient.send({ to, subject });
}
export async function chargePayment(userId: string, amount: number): Promise {
return stripeClient.charge(userId, amount);
}
// workflow.ts — deterministic orchestration:
import { proxyActivities, sleep, defineSignal, setHandler } from "@temporalio/workflow";
import type * as activities from "./activities";
const { sendEmail, chargePayment } = proxyActivities({
startToCloseTimeout: "10 seconds",
retry: { maximumAttempts: 5, initialInterval: "1s", backoffCoefficient: 2 },
});
// Signals:
const cancelSignal = defineSignal("cancel");
export async function orderWorkflow(order: Order): Promise {
let cancelled = false;
setHandler(cancelSignal, () => { cancelled = true; });
const txId = await chargePayment(order.userId, order.total);
if (cancelled) return;
await sleep("5 minutes"); // durable — survives restarts
await sendEmail(order.email, `Order confirmed. Transaction: ${txId}`);
}
// worker.ts:
import { Worker } from "@temporalio/worker";
import * as activities from "./activities";
const worker = await Worker.create({
taskQueue: "orders",
workflowsPath: require.resolve("./workflow"),
activities,
});
await worker.run();
5. Local Dev, Namespaces & Production Patterns
Run Temporal locally with Docker, namespaces for multi-tenancy, and versioning workflows
# Local development with Temporal CLI:
# brew install temporal
temporal server start-dev --db-filename temporal.db
# Opens UI at http://localhost:8233
# Docker Compose for local dev:
# services:
# temporal:
# image: temporalio/auto-setup:1.24
# ports: ["7233:7233", "8233:8233"]
# environment:
# DB: sqlite
# Create namespace (logical isolation — separate task queues, history, rate limits):
temporal operator namespace create --namespace production
# Temporal Cloud connection (Go):
c, _ := client.Dial(client.Options{
HostPort: "your-namespace.tmprl.cloud:7233",
Namespace: "your-namespace",
ConnectionOptions: client.ConnectionOptions{
TLS: &tls.Config{},
},
Credentials: client.NewAPIKeyStaticCredentials("your-api-key"),
})
// Workflow versioning — safely evolve workflow logic with running executions:
v := workflow.GetVersion(ctx, "payment-v2", workflow.DefaultVersion, 1)
if v == workflow.DefaultVersion {
workflow.ExecuteActivity(ctx, OldChargeActivity, ...).Get(ctx, nil)
} else {
workflow.ExecuteActivity(ctx, NewChargeActivity, ...).Get(ctx, nil)
}
// GetVersion replays correctly — old executions use DefaultVersion path,
// new executions use version 1 path. Remove the branch once all old executions complete.
// Testing with testsuite (no Temporal server needed):
suite := testsuite.WorkflowTestSuite{}
env := suite.NewTestWorkflowEnvironment()
env.RegisterActivity(activities)
env.OnActivity(ChargePaymentActivity, mock.Anything, mock.Anything).Return("txn-123", nil)
env.ExecuteWorkflow(OrderWorkflow, testOrder)
suite.True(env.IsWorkflowCompleted())
Track Go and distributed systems tooling at ReleaseRun. Related: Kubernetes Reference | ArgoCD Reference | Kafka Reference
🔍 Free tool: npm Package Health Checker — check @temporalio/client and worker packages for known CVEs and active maintenance.
Founded
2023 in London, UK
Contact
hello@releaserun.com