@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 runoutput dev, the CLI starts Docker Compose which launches a worker container. The worker:
- Scans your project for workflow files (
workflow.js), step files (steps.js), evaluator files (evaluators.js), and shared components inshared/steps/andshared/evaluators/ - Creates a catalog of all discovered workflows and their activities with metadata (name, description, schemas)
- Connects to Temporal at the configured address
- Starts processing workflow executions
File Discovery
The worker scans yoursrc/ 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 underoutputai.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
activityInfois Temporal’s Activity execution info object. See Temporal’sactivity.Inforeference for all fields.workflowDetailsis Output’s serializable subset of Temporal’sworkflow.WorkflowInfo. It includesworkflowId,runId,workflowType,parent,root,firstExecutionRunId,continuedFromExecutionRunId,startTime,runStartTime, andattempt.outputActivityKindis Output metadata for activity hooks and custom events emitted from activities. Possible values arestep,evaluator, andinternal_step.aggregationscontains activity-scoped attribute totals collected while the step or evaluator ran. It isnullwhen no attributes were collected.
on(eventName, handler) keep framework context separate from event-specific data:
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.
http:request stores its request fields under payload, while eventId and activity context remain at the top level.
onError payload by source
activity—eventId,eventDate,source,activityInfo,workflowDetails,outputActivityKind,aggregations,errorworkflow—eventId,eventDate,source,workflowDetails,errorruntime—eventId,eventDate,source,error
$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
payload:
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:
$ENRICHMENT_API_TOKEN from process.env.ENRICHMENT_API_TOKEN inside the activity. Workflow history and trace files store the placeholder, not the token value.
sendPostRequestAndAwaitWebhook
Send a POST request and pause the workflow until a webhook response comes back. See External Integration for the full guide./workflow/:id/feedback. Once the external system sends feedback via the API, the workflow resumes with the received payload.
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:
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: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:
activityIdactivityTypeenvironmentlabellevelmessagemetadatarunIdservicesplatstacktimestampworkflowIdworkflowType
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:Logger.createLogger() when several logs should share the same namespace:
Worker output
The worker uses Winston for structured logging. Development (colorized, human-readable):NODE_ENV=production, JSON):