Skip to main content
The @outputai/core package is the foundation of every Output app. It gives you workflow, step, and evaluator — the three building blocks for defining what your app does. It also provides the worker runtime that connects to Temporal and runs your workflows in production.

What’s in the Package

For full details on workflow, step, and evaluator, see Workflows, Steps, and Evaluators.

Worker Runtime

When you run output dev, the CLI starts Docker Compose which launches a worker container. The worker:
  1. Scans your project for workflow files (workflow.js), step files (steps.js), evaluator files (evaluators.js), and shared components in shared/steps/ and shared/evaluators/
  2. Creates a catalog of all discovered workflows and their activities with metadata (name, description, schemas)
  3. Connects to Temporal at the configured address
  4. Starts processing workflow executions

File Discovery

The worker scans your src/ directory for these file patterns: Each discovered component is logged during startup:

Architecture

Output is built on Temporal.io for durable execution. Your abstractions map to Temporal primitives: When you call a step from a workflow, Output executes it as a durable activity with automatic retries, schema validation, and tracing. If the worker crashes mid-execution, Temporal replays the workflow and skips already-completed steps.

Hooks

Register handlers in hook files that the worker loads at startup (list paths under outputai.hookFiles in package.json). Import from @outputai/core/hooks. The framework wraps each handler in a try/catch: failures are logged and do not stop the worker or workflow runs. Every hook payload includes an eventId — a UUID v4 stamped per emit — and an eventDate, the millisecond epoch timestamp for when the event was emitted. Use eventId as a stable per-emit idempotency key for downstream dedup (webhook retry handling, ClickHouse ReplacingMergeTree, audit logs, etc.). Distinct emits — including http:request and cost:http:request for the same fetch — receive distinct eventIds. Context objects on hook payloads
  • activityInfo is Temporal’s Activity execution info object. See Temporal’s activity.Info reference for all fields.
  • workflowDetails is Output’s serializable subset of Temporal’s workflow.WorkflowInfo. It includes workflowId, runId, workflowType, parent, root, firstExecutionRunId, continuedFromExecutionRunId, startTime, runStartTime, and attempt.
  • outputActivityKind is Output metadata for activity hooks and custom events emitted from activities. Possible values are step, evaluator, and internal_step.
  • aggregations contains activity-scoped attribute totals collected while the step or evaluator ran. It is null when no attributes were collected.
SDK and custom event envelopes Events received with on(eventName, handler) keep framework context separate from event-specific data:
Use emit(eventName, payload) from a step or evaluator to publish a custom event. The payload is optional and can be any JavaScript value. If you call emit() outside a step or evaluator (and therefore outside an activity context), the event still includes eventId, eventDate, and payload, but it does not include activityInfo, workflowDetails, or outputActivityKind.
SDK events use the same envelope. For example, http:request stores its request fields under payload, while eventId and activity context remain at the top level.
onError payload by source
  • activityeventId, eventDate, source, activityInfo, workflowDetails, outputActivityKind, aggregations, error
  • workfloweventId, eventDate, source, workflowDetails, error
  • runtimeeventId, eventDate, source, error
The internal $catalog workflow is excluded from lifecycle and error hooks. Activity hooks include internal activities, identified by outputActivityKind: 'internal_step'.

HTTP from Workflows

sendHttpRequest

Send HTTP requests directly from workflow code (not from steps):
workflow.ts
For POST or PUT requests, include a payload:
sendHttpRequest is only callable from within workflows. Steps and evaluators can make HTTP requests directly using fetch or any HTTP client.
By default, sendHttpRequest returns only response metadata: url, status, statusText, and ok. Use responseOptions.includeHeaders to include response headers and responseOptions.includeBody to include the body. Included response headers are redacted automatically; response bodies are returned as-is. Use $ENV_VAR_NAME placeholders for secret header values:
The worker resolves $ENRICHMENT_API_TOKEN from process.env.ENRICHMENT_API_TOKEN inside the activity. Workflow history and trace files store the placeholder, not the token value.
sendHttpRequest is not primarily designed for secure or sensitive HTTP exchanges. Misconfigured requests can leak keys, tokens, cookies, signed URLs, payload data, or response bodies into Temporal history and trace files. Use $ENV_VAR_NAME placeholders for request header secrets, avoid secrets in URLs and payloads, and only set responseOptions.includeBody: true when the response body is safe to persist.

sendPostRequestAndAwaitWebhook

Send a POST request and pause the workflow until a webhook response comes back. See External Integration for the full guide.
The workflow pauses after sending the request and waits for a response at /workflow/:id/feedback. Once the external system sends feedback via the API, the workflow resumes with the received payload.
sendPostRequestAndAwaitWebhook is not primarily designed for secure or sensitive HTTP exchanges. Its request arguments are stored in Temporal history and can appear in trace files, so secrets can leak if they are placed directly in URLs, payloads, or literal headers. Use $ENV_VAR_NAME placeholders for request header secrets and avoid secrets in URLs and payloads.

File Structure

Each workflow lives in its own directory:

Environment Variables

The worker reads these environment variables:

Connection and catalog

Worker concurrency and polling

These map to Temporal worker slots, pollers, and tuners. See Worker Tuning for details and examples.

Activity heartbeating

The worker sends Activity Heartbeats to the Temporal Service so it knows the activity is still making progress. If no heartbeat is received within the activity’s Heartbeat Timeout (set per activity in workflow options, e.g. heartbeatTimeout in proxyActivities), the server considers the activity timed out and may schedule another Activity Task Execution per the retry policy. That makes heartbeats important during deploys: when a worker restarts, the server detects missing heartbeats and retries on another worker instead of waiting for the full Start-To-Close Timeout. Set each activity’s Heartbeat Timeout longer than OUTPUT_ACTIVITY_HEARTBEAT_INTERVAL_MS so the server does not time out before the next heartbeat.

Tracing

Monitoring

Logging

@outputai/core exports a Logger object that you can call from both workflows and steps. Use the same shape as the internal worker logger: a string message plus an optional metadata object. Under the hood, Output routes all logs into the worker’s Winston logger. Workflow logs cross the Temporal sandbox through workflow sinks, while step logs use an activity-scoped bridge. Both paths end up in the same worker log hooks, so the output format, namespaces, and log level filtering stay consistent with the rest of the runtime.
workflow.ts
steps.ts

Log levels

Output exposes Winston’s default npm log levels: error, warn, info, http, verbose, debug, and silly. See Winston’s logging levels documentation for the priority order. Lower-priority logs are filtered according to the configured worker log level. Use OUTPUT_LOG_LEVEL to control what the worker emits:
When unset, development defaults to debug and production defaults to info.

Metadata

The second logger argument is metadata. It should be a plain object with fields you want attached to the log record:
Output automatically adds execution fields such as workflowId, workflowType, runId, activityId, and activityType when it writes the log. Some metadata field names are reserved because Winston, Output log hooks, or log formatting use them internally. Output drops these fields from logger metadata before building the final log message:
  • activityId
  • activityType
  • environment
  • label
  • level
  • message
  • metadata
  • runId
  • service
  • splat
  • stack
  • timestamp
  • workflowId
  • workflowType
If you need to include a value with one of those meanings, use a domain-specific name instead, such as providerMessage, errorStack, or sourceTimestamp.

Namespace

Logs from within workflows have the “Workflow” namespace, while those from activity context have “Activity”. Namespace is a discrete field in the JSON output in production, and a prefix for the string message in development. The namespace can be customized by setting it in the metadata:
Use Logger.createLogger() when several logs should share the same namespace:
Metadata on an individual log call can still override the logger’s default namespace:

Worker output

The worker uses Winston for structured logging. Development (colorized, human-readable):
Production (NODE_ENV=production, JSON):
The same logger calls are formatted as human-readable logs in development and JSON logs in production.

API Reference

For complete TypeScript API documentation, see the Core Module API Reference.