> ## Documentation Index
> Fetch the complete documentation index at: https://docs.output.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# @outputai/core

> Workflow orchestration, worker runtime, and the building blocks for every Output app

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

```typescript theme={null}
import {
  // Building blocks
  workflow,
  step,
  evaluator,

  // Evaluation result types
  EvaluationBooleanResult,
  EvaluationNumberResult,
  EvaluationStringResult,
  EvaluationFeedback,

  // HTTP from workflows
  sendHttpRequest,
  sendPostRequestAndAwaitWebhook,

  // Logging from workflows and steps
  Logger,

  // Error types
  FatalError,
  ValidationError,

  // Zod (re-exported for convenience)
  z
} from '@outputai/core';
```

| Export                           | Description                                                              |
| -------------------------------- | ------------------------------------------------------------------------ |
| `workflow`                       | Define orchestration logic that coordinates steps                        |
| `step`                           | Define units of work that handle I/O (API calls, database queries, etc.) |
| `evaluator`                      | Define steps that score content and return evaluation results            |
| `EvaluationBooleanResult`        | Pass/fail evaluation result                                              |
| `EvaluationNumberResult`         | Numeric score evaluation result                                          |
| `EvaluationStringResult`         | Category/label evaluation result                                         |
| `EvaluationFeedback`             | Structured feedback (issue + suggestion + priority)                      |
| `sendHttpRequest`                | Send HTTP requests from within workflows                                 |
| `sendPostRequestAndAwaitWebhook` | Send a POST and pause until a webhook responds                           |
| `Logger`                         | Structured logger that works from both workflows and steps               |
| `FatalError`                     | Non-retryable error — stops immediately                                  |
| `ValidationError`                | Schema validation failure — stops immediately                            |
| `z`                              | Zod schema library for input/output validation                           |

For full details on `workflow`, `step`, and `evaluator`, see [Workflows](/workflows), [Steps](/steps), and [Evaluators](/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:

| Location                    | Purpose                                        |
| --------------------------- | ---------------------------------------------- |
| `workflows/*/workflow.js`   | Workflow definition                            |
| `workflows/*/steps.js`      | Step implementations                           |
| `workflows/*/evaluators.js` | Evaluator implementations                      |
| `shared/steps/*.js`         | Shared steps (importable by any workflow)      |
| `shared/evaluators/*.js`    | Shared evaluators (importable by any workflow) |

Each discovered component is logged during startup:

```
[Scanner] [info] Workflow loaded {"name":"lead_enrichment","path":"src/workflows/lead_enrichment/workflow.js"}
[Scanner] [info] Component loaded {"type":"step","name":"lookupCompany","path":"src/workflows/lead_enrichment/steps.js"}
```

### Architecture

Output is built on [Temporal.io](https://temporal.io) for durable execution. Your abstractions map to Temporal primitives:

| Output.ai     | Temporal |
| ------------- | -------- |
| `workflow()`  | Workflow |
| `step()`      | Activity |
| `evaluator()` | Activity |

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 `eventId`s.

| Function                 | When it runs                                                                                  | Payload                                                                                                                                                      |
| ------------------------ | --------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `onError`                | Activity, workflow, or runtime error                                                          | Object always includes `eventId`, `eventDate`, `source`, and `error`. Other fields depend on `source` — see below and [Error Hooks](/operations/error-hooks) |
| `onBeforeWorkerStart`    | Once after hooks, workflows, and activities are loaded; before the Temporal worker is created | None                                                                                                                                                         |
| `onWorkflowStart`        | A user workflow execution starts                                                              | `eventId`, `eventDate`, `workflowDetails`                                                                                                                    |
| `onWorkflowEnd`          | A user workflow execution completes successfully                                              | `eventId`, `eventDate`, `workflowDetails`                                                                                                                    |
| `onWorkflowError`        | A user workflow execution fails                                                               | `eventId`, `eventDate`, `workflowDetails`, `error`                                                                                                           |
| `onActivityStart`        | An Output activity starts, including internal activities                                      | `eventId`, `eventDate`, `activityInfo`, `workflowDetails`, `outputActivityKind`                                                                              |
| `onActivityEnd`          | An Output activity completes successfully, including internal activities                      | `eventId`, `eventDate`, `activityInfo`, `workflowDetails`, `outputActivityKind`, `aggregations`                                                              |
| `onActivityError`        | An Output activity fails, including internal activities                                       | `eventId`, `eventDate`, `activityInfo`, `workflowDetails`, `outputActivityKind`, `aggregations`, `error`                                                     |
| `on(eventName, handler)` | SDK and custom events                                                                         | An envelope containing `eventId`, `eventDate`, and your original value under `payload`; activity context fields are included when available                  |

**Context objects on hook payloads**

* `activityInfo` is Temporal's Activity execution info object. See Temporal's [`activity.Info`](https://typescript.temporal.io/api/interfaces/activity.Info) reference for all fields.
* `workflowDetails` is Output's serializable subset of Temporal's [`workflow.WorkflowInfo`](https://typescript.temporal.io/api/interfaces/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:

```typescript theme={null}
{
  eventId: string;
  eventDate: number;
  activityInfo?: Info;
  workflowDetails?: WorkflowDetails;
  outputActivityKind?: string;
  payload: unknown;
}
```

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`.

```typescript theme={null}
import { emit, on } from '@outputai/core/hooks';

emit('company:enriched', {
  companyId: 'company-123',
  domain: 'example.com'
});

on<{ companyId: string; domain: string }>('company:enriched', ({ eventId, workflowDetails, payload }) => {
  if (!workflowDetails || !payload) {
    return;
  }

  console.log('Company enriched', {
    eventId,
    workflowId: workflowDetails.workflowId,
    companyId: payload.companyId,
    domain: payload.domain
  });
});
```

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.

```typescript theme={null}
type WorkflowDetails = {
  workflowId: string;
  runId: string;
  workflowType: string;
  parent?: { workflowId: string; runId: string; namespace: string };
  root?: { workflowId: string; runId: string };
  firstExecutionRunId: string;
  continuedFromExecutionRunId?: string;
  startTime: number;
  runStartTime: number;
  attempt: number;
};
```

```typescript theme={null}
type Aggregations = {
  cost: { total: number };
  tokens: {
    total: number;
    [tokenType: string]: number | undefined;
  };
  httpRequests: { total: number };
};
```

**`onError` payload by `source`**

* **`activity`** — `eventId`, `eventDate`, `source`, `activityInfo`, `workflowDetails`, `outputActivityKind`, `aggregations`, `error`
* **`workflow`** — `eventId`, `eventDate`, `source`, `workflowDetails`, `error`
* **`runtime`** — `eventId`, `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):

```typescript workflow.ts theme={null}
import { workflow, sendHttpRequest } from '@outputai/core';
import { EnrichmentInput, EnrichmentOutput } from './types.js';

export default workflow({
  name: 'lead_enrichment',
  inputSchema: EnrichmentInput,
  outputSchema: EnrichmentOutput,
  fn: async (input) => {
    const response = await sendHttpRequest({
      url: 'https://api.example.com/companies',
      method: 'GET',
      headers: { Authorization: 'Bearer $ENRICHMENT_API_TOKEN' },
      responseOptions: { includeBody: true }
    });

    return {
      company: response.body.name,
      summary: response.body.description
    };
  }
});
```

For POST or PUT requests, include a `payload`:

```typescript theme={null}
const response = await sendHttpRequest({
  url: 'https://api.example.com/companies',
  method: 'POST',
  payload: { domain: 'acme.com' },
  responseOptions: { includeBody: true }
});
```

<Warning>
  `sendHttpRequest` is only callable from within workflows. Steps and evaluators can make HTTP requests directly using fetch or any HTTP client.
</Warning>

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:

```typescript theme={null}
await sendHttpRequest({
  url: 'https://api.example.com/companies',
  method: 'GET',
  headers: {
    Authorization: 'Bearer $ENRICHMENT_API_TOKEN'
  }
});
```

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.

<Warning>
  `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.
</Warning>

### sendPostRequestAndAwaitWebhook

Send a POST request and pause the workflow until a webhook response comes back. See [External Integration](/workflows/external-integration#pattern-1-workflow-initiates-the-request) for the full guide.

```typescript theme={null}
import { sendPostRequestAndAwaitWebhook } from '@outputai/core';

const response = await sendPostRequestAndAwaitWebhook({
  url: 'https://your-app.com/callback',
  payload: { data: 'value' },
  headers: { Authorization: 'Bearer $CALLBACK_API_TOKEN' }
});
```

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.

<Warning>
  `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.
</Warning>

## File Structure

Each workflow lives in its own directory:

```text theme={null}
src/workflows/
└── lead_enrichment/
    ├── workflow.ts           # Workflow definition
    ├── steps.ts              # Step implementations
    ├── evaluators.ts         # Evaluators (optional)
    ├── types.ts              # Zod schemas
    ├── prompts/              # LLM prompt templates
    │   └── generate_summary@v1.prompt
    └── scenarios/            # Test scenarios
        └── test_input.json
```

## Environment Variables

The worker reads these environment variables:

### Connection and catalog

| Variable             | Default          | Description                                              |
| -------------------- | ---------------- | -------------------------------------------------------- |
| `OUTPUT_CATALOG_ID`  | —                | **Required.** Name of the local catalog (use your email) |
| `TEMPORAL_ADDRESS`   | `localhost:7233` | Temporal backend address                                 |
| `TEMPORAL_NAMESPACE` | `default`        | Temporal namespace                                       |
| `TEMPORAL_API_KEY`   | —                | API key for remote Temporal (blank for local)            |

### Worker concurrency and polling

These map to Temporal worker slots, pollers, and tuners. See [Worker Tuning](/operations/worker-tuning) for details and examples.

| Variable                                           | Default | Description                                                     |
| -------------------------------------------------- | ------- | --------------------------------------------------------------- |
| `TEMPORAL_MAX_CONCURRENT_ACTIVITY_TASK_EXECUTIONS` | `40`    | Max Activity Task executions at once.                           |
| `TEMPORAL_MAX_CONCURRENT_WORKFLOW_TASK_EXECUTIONS` | `200`   | Max Workflow Task executions at once.                           |
| `TEMPORAL_MAX_CACHED_WORKFLOWS`                    | `1000`  | Max number of Workflow Executions in the sticky workflow cache. |
| `TEMPORAL_MAX_CONCURRENT_ACTIVITY_TASK_POLLS`      | `5`     | Max concurrent pollers for Activity Task Queues.                |
| `TEMPORAL_MAX_CONCURRENT_WORKFLOW_TASK_POLLS`      | `5`     | Max concurrent pollers for Workflow Task Queues.                |
| `TEMPORAL_WORKER_TUNER`                            | —       | JSON-encoded Temporal Worker tuner configuration.               |

### Activity heartbeating

The worker sends [Activity Heartbeats](https://docs.temporal.io/develop/typescript/failure-detection#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](https://docs.temporal.io/tasks#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.

| Variable                                | Default          | Description                                                                                                              |
| --------------------------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------ |
| `OUTPUT_ACTIVITY_HEARTBEAT_INTERVAL_MS` | `120000` (2 min) | How often the worker sends a heartbeat while an activity is running. Must be less than the activity’s Heartbeat Timeout. |
| `OUTPUT_ACTIVITY_HEARTBEAT_ENABLED`     | `true`           | Whether to send heartbeats. Set to `false` only if you do not use long-running activities or Heartbeat Timeouts.         |

### Tracing

| Variable                        | Default           | Description                                                                |
| ------------------------------- | ----------------- | -------------------------------------------------------------------------- |
| `OUTPUT_TRACE_LOCAL_ON`         | —                 | Enable local trace files — see [Tracing](/operations/tracing)              |
| `OUTPUT_TRACE_REMOTE_ON`        | —                 | Enable S3 trace upload (requires Redis + AWS)                              |
| `OUTPUT_TRACE_HOST_PATH`        | —                 | Host path for Docker trace file mounting                                   |
| `OUTPUT_TRACE_REMOTE_S3_BUCKET` | —                 | S3 bucket for remote traces                                                |
| `OUTPUT_REDIS_URL`              | —                 | Redis address (required for remote tracing)                                |
| `OUTPUT_REDIS_TRACE_TTL`        | `604800` (7 days) | TTL in seconds for Redis keys holding workflow trace data before S3 upload |
| `OUTPUT_AWS_REGION`             | —                 | AWS region for S3 bucket                                                   |
| `OUTPUT_AWS_ACCESS_KEY_ID`      | —                 | AWS access key                                                             |
| `OUTPUT_AWS_SECRET_ACCESS_KEY`  | —                 | AWS secret key                                                             |

### Monitoring

| Variable                              | Default              | Description                                                                                                         |
| ------------------------------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `OUTPUT_WORKER_TELEMETRY_INTERVAL_MS` | `0`                  | Interval between worker telemetry log messages. `0` means off.                                                      |
| `TEMPORAL_SHUTDOWN_GRACE_TIME`        | Temporal SDK default | Time to let in-flight work drain after worker shutdown starts. Accepts milliseconds or duration strings like `15s`. |
| `TEMPORAL_SHUTDOWN_FORCE_TIME`        | Temporal SDK default | Extra time before forceful worker shutdown. Accepts milliseconds or duration strings like `15s`.                    |

## 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.

```typescript workflow.ts theme={null}
import { workflow, Logger, z } from '@outputai/core';

export default workflow({
  name: 'lead_enrichment',
  outputSchema: z.object({
    summary: z.string()
  }),
  fn: async () => {
    Logger.info('Starting enrichment', { source: 'crm' });

    // ...

    Logger.debug('Generated summary', { tokenCount: 842 });
    return { summary: 'Acme Corp is a B2B SaaS company...' };
  }
});
```

```typescript steps.ts theme={null}
import { step, Logger, z } from '@outputai/core';

export const lookupCompany = step({
  name: 'lookup_company',
  inputSchema: z.object({
    domain: z.string()
  }),
  outputSchema: z.object({
    name: z.string()
  }),
  fn: async ({ domain }) => {
    Logger.info('Looking up company', { domain });

    const response = await fetch(`https://api.example.com/companies/${domain}`);
    Logger.http('Company lookup response', { status: response.status });

    return response.json();
  }
});
```

### Log levels

Output exposes Winston's default npm log levels:

`error`, `warn`, `info`, `http`, `verbose`, `debug`, and `silly`.

See Winston's [logging levels](https://github.com/winstonjs/winston#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:

```bash theme={null}
OUTPUT_LOG_LEVEL=debug
```

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:

```typescript theme={null}
Logger.warn('Retrying provider request', {
  provider: 'anthropic',
  attempt: 2,
  requestId: 'lead-enrichment-123'
});
```

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:

```js theme={null}
Logger.warn('My awesome message', { namespace: 'Custom' });
```

Use `Logger.createLogger()` when several logs should share the same namespace:

```js theme={null}
const log = Logger.createLogger('LLM');

log.info('Generating completion', { model: 'claude-sonnet-4' });
log.warn('Provider request retrying', { attempt: 2 });
```

Metadata on an individual log call can still override the logger's default namespace:

```js theme={null}
const log = Logger.createLogger('LLM');

log.info('Using fallback provider', { namespace: 'LLM Fallback' });
```

### Worker output

The worker uses Winston for structured logging.

**Development** (colorized, human-readable):

```
[info] Worker: Loading workflows... { callerDir: "/app/src" }
[info] Scanner: Workflow loaded { name: "lead_enrichment", path: "..." }
```

**Production** (`NODE_ENV=production`, JSON):

```json theme={null}
{"level":"info","message":"Loading workflows...","namespace":"Worker","service":"output-worker","environment":"production","timestamp":"..."}
```

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](https://output-ai-reference-code-docs.onrender.com/modules/core_src.html).
