> ## Documentation Index
> Fetch the complete documentation index at: https://growthx-changeset-release-main.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# v0.10.0 → v0.11.0

> Upgrading Output.ai projects from v0.10.0 to v0.11.0: workflow results and error handling, parsed component schemas, Temporal worker cutover, and other breaking changes.

This guide covers breaking changes across `@outputai/core`, `@outputai/http`, `@outputai/llm`, `@outputai/cli`, and `output-api`. The main ones are unwrapped workflow results and error handling, Zod schemas that return parsed values, and a Temporal worker runtime that is not replay-compatible with open v0.10.x executions.

## API HTTP request logging

The API server was refactored to remove the `morgan` dependency and emit richer request logs. The public HTTP API contract is unchanged, but the shape, level, and message of the per-request log lines the server emits are different. This only affects you if you consume `output-api` logs in an observability tool (Datadog, CloudWatch, Loki, etc.) or match on them in scripts.

### Log level now derives from response status

This is the most impactful change. Previously every request was logged at the `http` level regardless of outcome. Now the level is chosen from the response status:

| Status    | Level (before) | Level (after) |
| --------- | -------------- | ------------- |
| 2xx / 3xx | `http`         | `http`        |
| 4xx       | `http`         | `warn`        |
| 5xx       | `http`         | `error`       |

If you have alerts or filters keyed on winston log levels, they will now behave differently: any alert on `error` or `warn` will fire on 4xx/5xx HTTP responses that previously logged at `http`.

### The `status` field was renamed to `statusCode`

The structured metadata field carrying the numeric response status changed name, for consistency with the other fields the logger emits.

```diff theme={null}
- "status": 200
+ "statusCode": 200
```

If you use Datadog, map `statusCode` to the standard `http.status_code` attribute with a remapper in your log pipeline. That keeps the built-in HTTP status facet working without emitting a dotted field name from the application.

### The log message changed from a constant to a request summary

Previously the message was the constant string `"HTTP request"`, with all detail in the metadata. Now the message is a human-readable summary and the structured record is still attached as metadata.

```diff theme={null}
- "HTTP request"
+ "POST /workflow 200 12ms"
```

### `responseTime` is now an integer

`responseTime` is now whole milliseconds (measured with `Date.now()`) rather than a sub-millisecond float.

```diff theme={null}
- "responseTime": 12.347
+ "responseTime": 12
```

### Before and after

A successful request, before (v0.10.0):

```json theme={null}
{
  "level": "http",
  "message": "HTTP request",
  "method": "POST",
  "url": "/workflow",
  "status": 200,
  "contentLength": "42",
  "responseTime": 12.347,
  "requestId": "8f3c...",
  "workflowName": "simple"
}
```

The same request, after (v0.11.0):

```json theme={null}
{
  "level": "http",
  "message": "POST /workflow 200 12ms",
  "method": "POST",
  "url": "/workflow",
  "statusCode": 200,
  "contentLength": "42",
  "responseTime": 12,
  "requestId": "8f3c...",
  "workflowName": "simple"
}
```

A server error, before — logged at `http`:

```json theme={null}
{
  "level": "http",
  "message": "HTTP request",
  "status": 500,
  "errorType": "Error",
  "errorMessage": "Something went wrong"
}
```

After — logged at `error`, with the renamed field:

```json theme={null}
{
  "level": "error",
  "message": "POST /workflow 500 34ms",
  "statusCode": 500,
  "errorType": "Error",
  "errorMessage": "Something went wrong"
}
```

### Migration steps

#### Update alerts and level-based filters

If you alert or filter on winston levels, audit those rules. HTTP 4xx now arrives at `warn` and 5xx at `error`. Decide whether that is what you want:

* To keep alerting only on application errors, scope alerts to exclude HTTP request logs (for example, filter on the presence of `statusCode`).
* To alert on failing requests, this is now available directly from the log level with no extra query.

#### Update queries and facets that reference `status`

Anywhere you query, facet, or dashboard on the request-log `status` field, switch to `statusCode`.

```diff theme={null}
# Datadog / log query
- @status:500
+ @statusCode:500
```

In Datadog, add a remapper in your log pipeline to map `statusCode` onto the standard `http.status_code` attribute. That attribute maps to the built-in HTTP status facet, so once the remapper is in place you can query on `@http.status_code:500` and reuse the standard facet instead of defining a custom one.

#### Update anything that matches the message string

Scripts, log processors, or saved searches that match the literal message `"HTTP request"` will no longer match. Match on a structural field instead (for example, the presence of `statusCode` or `requestId`), or update the matcher to the new summary format `<METHOD> <URL> <STATUS> <ms>ms`.

#### Adjust `responseTime` consumers expecting sub-millisecond precision

If a dashboard or aggregation relied on fractional milliseconds in `responseTime`, note that values are now whole milliseconds.

### API logging checklist

* Review winston level-based alerts and filters; 4xx now logs at `warn`, 5xx at `error`.
* Rename `status` to `statusCode` in log queries, facets, and dashboards; in Datadog, add a remapper from `statusCode` to `http.status_code`.
* Replace any match on the constant message `"HTTP request"` with a structural field or the new summary format.
* Confirm `responseTime` consumers tolerate integer milliseconds.

## Hook event changes

This section applies to hook files that import from `@outputai/core/hooks`.

### `on()` now receives an event envelope

In v0.10.x, SDK event fields were merged into the same object as framework metadata. In v0.11.0, framework metadata remains at the top level and event-specific data is available under `payload`.

This affects every handler registered with `on()`, including:

* `http:request`
* `cost:http:request`
* `cost:llm:request`
* Custom events

Lifecycle hooks such as `onWorkflowStart`, `onActivityEnd`, and `onError` remain flat.

#### Before

```ts theme={null}
import { on } from '@outputai/core/hooks';
import type { HttpRequestEvent } from '@outputai/http';

on<HttpRequestEvent>( 'http:request', event => {
  console.log( event.eventId, event.workflowDetails.workflowId, event.method, event.url );
} );
```

#### After

```ts theme={null}
import { on } from '@outputai/core/hooks';
import type { HttpRequestEvent } from '@outputai/http';

on<HttpRequestEvent>( 'http:request', event => {
  if ( !event.workflowDetails || !event.payload ) {
    return;
  }

  console.log(
    event.eventId,
    event.workflowDetails.workflowId,
    event.payload.method,
    event.payload.url
  );
} );
```

The envelope has this shape:

```ts theme={null}
{
  eventId: string;
  eventDate: number;
  activityInfo?: Info;
  workflowDetails?: WorkflowDetails;
  outputActivityKind?: string;
  payload: T | undefined;
}
```

The generic passed to `on<T>()` still describes the event-specific data. The difference is that `T` now types `event.payload` instead of fields on `event` itself.

Because custom events can omit their payload, check `event.payload` before reading its fields. SDK events such as `http:request` and cost events always supply one, but the public handler type still represents the optional case.

v0.11.0 also adds `emit(eventName, payload?)` for publishing custom events from an Output activity:

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

emit( 'company:enriched', { companyId: 'company-123' } );

on<{ companyId: string }>( 'company:enriched', event => {
  if ( !event.payload ) {
    return;
  }

  console.log( event.payload.companyId );
} );
```

When an event is emitted outside an activity context, `activityInfo`, `workflowDetails`, and `outputActivityKind` are omitted. These fields are therefore optional on `ExternalHookPayload<T>`; check any context field before using it.

### Activity lifecycle hooks no longer include `aggregations`

`onActivityEnd`, `onActivityError`, and activity-sourced `onError` payloads no longer include the `aggregations` field. Remove reads of `event.aggregations` from hook handlers and use the dedicated cost and request events when those measurements are needed.

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

on( 'cost:llm:request', event => {
  console.log( event.payload );
} );
```

### Workflow error hooks receive serialized objects

Workflow errors are serialized before crossing Temporal's workflow sandbox boundary. The `error` field passed to `onWorkflowError` and workflow-sourced `onError` handlers is now a plain error-like object rather than an `Error` instance.

```ts theme={null}
import { onError } from '@outputai/core/hooks';

onError( event => {
  if ( event.source === 'workflow' ) {
    console.log( event.error.name, event.error.message );
    // event.error instanceof Error === false
  }
} );
```

Activity and runtime error hooks continue to receive `Error` instances.

### Update imported hook payload types

The exported base and envelope types were simplified in v0.11.0. If your hook files import these types directly, make the following replacements:

* Replace `OnHookPayload<T>` with `ExternalHookPayload<T>`.
* Replace `OnHookEnvelope` with `ActivityPayloadBase`.
* Replace `ActivityHookPayload` with `ActivityPayloadBase`.

`HookPayloadBase` now contains only `eventId` and `eventDate`. Use `WorkflowPayloadBase` when you also need `workflowDetails`, or `ActivityPayloadBase` when you need the workflow and activity context fields.

#### Before

```ts theme={null}
import type {
  ActivityHookPayload,
  OnHookEnvelope,
  OnHookPayload
} from '@outputai/core/hooks';

type CompanyEnrichedEvent = OnHookPayload<{ companyId: string }>;
```

#### After

```ts theme={null}
import type {
  ActivityPayloadBase,
  ExternalHookPayload
} from '@outputai/core/hooks';

type CompanyEnrichedEvent = ExternalHookPayload<{ companyId: string }>;
```

### Activity lifecycle hooks include internal activities

`onActivityStart`, `onActivityEnd`, and `onActivityError` now run for internal activities as well as steps and evaluators. Internal activity payloads have:

```ts theme={null}
{
  outputActivityKind: 'internal_step'
}
```

If your handler should keep the v0.10.x behavior, add an explicit filter:

```ts theme={null}
import { onActivityStart } from '@outputai/core/hooks';

onActivityStart( event => {
  if ( event.outputActivityKind === 'internal_step' ) {
    return;
  }

  // Handle steps and evaluators.
} );
```

Review metrics, logs, webhooks, and state updates triggered by activity lifecycle hooks. Without a filter, internal activities now contribute to those side effects.

### `onError` excludes the internal catalog workflow

The internal `$catalog` workflow was already excluded from `onWorkflowStart`, `onWorkflowEnd`, and `onWorkflowError`. In v0.11.0, its workflow and activity errors are also excluded from `onError`.

No migration is needed unless an `onError` handler explicitly relied on `$catalog` failures. Worker startup and catalog-manager failures still surface as runtime errors. Monitor Temporal or API catalog health if you need to detect the catalog workflow being terminated after startup.

### Hook migration checklist

* Update every `on()` handler to read event-specific fields from `event.payload`.
* Guard `event.payload` before accessing its fields.
* Keep framework fields such as `eventId`, `workflowDetails`, and `activityInfo` at the top level.
* Remove reads of `aggregations` from activity lifecycle and error hooks.
* Treat workflow hook errors as serialized error-like objects rather than `Error` instances.
* Filter `outputActivityKind === 'internal_step'` in activity lifecycle hooks when internal activities should remain hidden.
* Remove assumptions that `onError` reports `$catalog` workflow failures.

## HTTP client changes

`@outputai/http` now exposes two explicitly named, instrumented clients:

* `outputFetch` for Fetch-compatible requests.
* `createKyClient` for a Ky client backed by `outputFetch`.

Ky was upgraded from v1 to v2 and both Ky and Undici are now peer dependencies. Most projects using the previous `httpClient` need import, option, and dependency updates.

### Upgrade and resolve the HTTP peers

Ky and Undici were previously private dependencies of `@outputai/http` and are now peers. Current npm and pnpm versions install compatible peers automatically, so upgrading the Output package is normally enough:

```bash theme={null}
npm install @outputai/http@^0.11
```

Use the equivalent command for your package manager, then commit the updated lockfile. Install `ky@^2` or `undici@^8` explicitly only if you need to pin a version or resolve a peer-version conflict.

### Rename HTTP exports

Update imports using this mapping:

| v0.10.x             | v0.11.0                                     |
| ------------------- | ------------------------------------------- |
| `fetch`             | `outputFetch`                               |
| `httpClient`        | `createKyClient`                            |
| `HTTPError`         | `ky.HTTPError`                              |
| `TimeoutError`      | `ky.TimeoutError`                           |
| `HttpClientOptions` | `ky.Options`                                |
| `RequestInfo`       | `undici.RequestInfo` or a native Fetch type |
| `RequestInit`       | `undici.RequestInit` or a native Fetch type |

For example:

```diff theme={null}
- import { httpClient, HTTPError, TimeoutError } from '@outputai/http';
+ import { createKyClient, ky } from '@outputai/http';

- const client = httpClient( {
+ const client = createKyClient( {
    timeout: 30_000
  } );

  try {
    await client.get( url );
  } catch ( error ) {
-   if ( error instanceof HTTPError ) {
+   if ( error instanceof ky.HTTPError ) {
      console.log( error.response.status );
    }
-   if ( error instanceof TimeoutError ) {
+   if ( error instanceof ky.TimeoutError ) {
      console.log( 'Request timed out' );
    }
  }
```

The complete Ky and Undici namespaces are available as `ky` and `undici` from `@outputai/http`.

### Update Ky v2 options

Replace Ky's `prefixUrl` option with `prefix`:

```diff theme={null}
- const client = httpClient( {
-   prefixUrl: 'https://api.example.com',
+ const client = createKyClient( {
+   prefix: 'https://api.example.com',
    timeout: 30_000
  } );
```

Review other Ky options against Ky 2 when upgrading. This is especially important for shared client factories that expose Ky's `Options` type.

See [Ky's official v2.0.0 migration guide](https://github.com/sindresorhus/ky/releases/tag/v2.0.0#migration-guide) for the complete list of changes, including empty-response JSON parsing, `beforeError` behavior, hook option normalization, `searchParams` merging, and `HTTPError.data`.

### Update Ky hook callbacks

Ky 2 passes one state object to every hook. Ky 1 `beforeRequest`, `beforeError`, and `afterResponse` callbacks commonly used positional arguments.

```diff theme={null}
- const beforeRequest = request => {
+ const beforeRequest = ( { request } ) => {
    request.headers.set( 'x-api-key', apiKey );
  };

- const beforeError = error => {
+ const beforeError = ( { error } ) => {
    console.error( error.response?.status );
    return error;
  };

- const afterResponse = async ( request, options, response ) => {
+ const afterResponse = async ( { request, options, response } ) => {
    await recordUsage( request, response );
  };
```

Types for hooks are also available through the Ky namespace:

```ts theme={null}
import { ky } from '@outputai/http';

const afterResponse = async ( { response }: ky.AfterResponseState ) => {
  // ...
};
```

Audit hook functions passed indirectly through shared option objects as well as inline hooks.

### Replace custom wrappers around the old fetch export

If your project created its own Ky factory solely to inject Output's old `fetch`, use `createKyClient` directly:

```diff theme={null}
- import ky from 'ky';
- import type { Options } from 'ky';
- import { fetch } from '@outputai/http';
+ import { createKyClient, ky } from '@outputai/http';

- export const createHttpClient = ( options: Options = {} ) =>
-   ky.create( { fetch: fetch as NonNullable<Options['fetch']>, ...options } );
+ export const createHttpClient = ( options: ky.Options = {} ) =>
+   createKyClient( options );
```

If the wrapper adds other defaults or hooks, keep the wrapper and pass those options to `createKyClient`.

### Update direct Fetch usage

Rename direct imports:

```diff theme={null}
- import { fetch } from '@outputai/http';
+ import { outputFetch } from '@outputai/http';

- const response = await fetch( url, init );
+ const response = await outputFetch( url, init );
```

`outputFetch` accepts URL strings, `URL` objects, and both Node and Undici `Request` objects. Node inputs are normalized before entering the Undici-backed implementation.

Keep `Request` and `FormData` from the same family within one call:

```ts theme={null}
import { outputFetch, undici } from '@outputai/http';

const form = new undici.FormData();
form.set( 'name', 'Ada' );

await outputFetch( 'https://api.example.com/users', {
  method: 'POST',
  body: form
} );
```

The same pattern works with Node's global `FormData`. Do not combine a Node `Request` with Undici `FormData`, or an Undici `Request` with Node `FormData`.

### Remove reliance on `undici.install()`

Importing `@outputai/http` no longer replaces `globalThis.fetch`, `Request`, `Response`, `Headers`, or `FormData`.

Code that imports an Undici dispatcher but calls global `fetch` should choose the HTTP implementation explicitly. Use `outputFetch` when the request should be traced:

```diff theme={null}
- import { undici } from '@outputai/http';
+ import { outputFetch, undici } from '@outputai/http';

  const dispatcher = new undici.EnvHttpProxyAgent( {
    headersTimeout: 15 * 60 * 1000,
    bodyTimeout: 15 * 60 * 1000
  } );

- const customFetch = ( input: string | URL | Request, init?: RequestInit ) =>
-   fetch( input, { ...init, dispatcher } as RequestInit );
+ const customFetch = (
+   input: undici.RequestInfo,
+   init?: undici.RequestInit
+ ) => outputFetch( input, { ...init, dispatcher } );
```

Use `undici.fetch` instead when tracing is not wanted. Do not assume the npm Undici dispatcher's types or behavior apply to Node's built-in Fetch implementation.

### Configure proxy behavior explicitly

Core workers no longer install a process-wide Undici dispatcher when proxy environment variables are present.

* `outputFetch` and `createKyClient` continue to use an `EnvHttpProxyAgent` and honor standard `HTTP_PROXY`, `HTTPS_PROXY`, and `NO_PROXY` variables.
* For direct npm Undici calls, pass an `undici.EnvHttpProxyAgent` or configure Undici's global dispatcher yourself.
* For Node's built-in Fetch, enable Node's environment proxy support at process startup with `NODE_USE_ENV_PROXY=1` and set the standard proxy variables.

Audit libraries that receive a custom `fetch` callback. Ensure the callback uses `outputFetch`, `undici.fetch`, or native Fetch intentionally rather than relying on the old import-time global replacement.

### HTTP migration checklist

* Upgrade `@outputai/http` and confirm the lockfile resolved Ky 2 and Undici 8.
* Rename `fetch` and `httpClient`.
* Update error and option types to the `ky` and `undici` namespaces.
* Replace `prefixUrl` with `prefix`.
* Convert Ky hooks to state-object parameters.
* Replace custom Ky factories that only injected Output's old `fetch`.
* Check custom dispatchers and Fetch callbacks for reliance on `undici.install()`.
* Configure proxy behavior for non-Output HTTP calls.
* Run type checking and HTTP integration tests after regenerating the lockfile.

## Component schemas now return parsed values

In v0.10.x, workflow, step, and evaluator schemas validated values, but component handlers and callers continued to receive the original values. In v0.11.0, components use the values returned by Zod:

* `inputSchema` parsing happens before `fn`, so the handler receives parsed input.
* `outputSchema` parsing happens after `fn`, so workflow and step callers receive parsed output.
* Zod coercions, transforms, defaults, and object-key handling now affect runtime values.

TypeScript mirrors that split: handlers are typed with `z.infer` for input (post-parse) and `z.input` for what they return (pre-parse). Callers pass `z.input` and receive `z.infer`.

For example:

```js theme={null}
const calculateTotal = step( {
  name: 'calculate_total',
  inputSchema: z.object( {
    count: z.coerce.number().default( 1 )
  } ),
  outputSchema: z.object( {
    total: z.number(),
    currency: z.string().default( 'USD' )
  } ),
  fn: async input => ( {
    total: input.count * 10,
    internalNote: 'not part of the public result'
  } )
} );

const result = await calculateTotal( {
  count: '2',
  requestId: 'request-123'
} );
// result => { total: 20, currency: 'USD' }
```

In v0.10.x, `input.count` was the string `'2'`, `input.requestId` remained available, `result.internalNote` was returned, and `currency` was absent because `fn` did not set it. In v0.11.0, `input.count` is the number `2`, Zod's default object behavior removes `requestId` from the handler input and `internalNote` from the caller result, and `outputSchema` fills `currency` with `'USD'`, so the returned result is `{ total: 20, currency: 'USD' }`.

Trace files still record the **raw** Temporal workflow arguments at start (before `inputSchema` parse) and the **parsed** output at end (after `outputSchema` parse).

### Transforms, replay, and continue-as-new

Because schemas run at the component boundary, `.transform()`, `z.coerce`, `.preprocess()`, and similar helpers are no longer documentation-only:

* **Determinism (workflow schemas):** Workflow code is replayed by Temporal. Any transform on a workflow `inputSchema` or `outputSchema` must be deterministic (no `Date.now()`, `Math.random()`, or other non-replay-safe sources). Prefer keeping workflow input as wire format (types, `.default()`, optional coerce) and putting one-shot shaping in a step or inside `fn` when possible.
* **Idempotency (re-parse):** Every workflow run parses its Temporal args again — including runs started with `continueAsNew`. The next run does not know it is a continuation. If you pass values that were already transformed, a non-idempotent transform runs twice (for example appending a suffix on each continue-as-new). Prefer idempotent transforms, or pass wire-format input into `continueAsNew` (the same shape you would pass when starting the workflow).
* **Catalog JSON Schema:** The worker catalog still converts Zod schemas with `z.toJSONSchema`. Bare `.transform()` often cannot be represented and may omit the schema from the catalog; `z.coerce` and `.pipe()` usually convert. Authors who control both the schema and catalog consumers should treat catalog JSON Schema as best-effort for advanced Zod features.

### Published workflow package types

`WorkflowFunctionWrapper`, `StepFunctionWrapper`, and `EvaluatorFunctionWrapper` are now typed as `Wrapper<InputSchema, OutputSchema>` (schema generics), not `Wrapper<Fn<In, Out>>`. Runtime is unchanged; TypeScript breaks for packages that still ship `.d.ts` built against the old arity (for example a published workflows catalog).

Rebuild and republish those packages against `@outputai/core` v0.11.0 so their declarations match, or release them together with the core bump.

### Migration steps

Review the `inputSchema` and `outputSchema` of every workflow, step, and evaluator:

* Update handlers and callers to use the parsed Zod input and output types (`z.infer` in handlers, `z.input` at call sites when schemas coerce or default).
* Check schemas using `.transform()`, `z.coerce`, `.default()`, `.catch()`, or `.preprocess()` for values that now change at runtime.
* Check object schemas for unknown properties that Zod strips by default. Use `z.looseObject( { ... } )` when those properties must be preserved.
* Ensure transforms on **workflow** schemas are deterministic for Temporal replay.
* Ensure workflow `inputSchema` transforms are idempotent if you `continueAsNew` with values that will be parsed again, or pass wire-format input instead.
* Republish any package that exports workflow/step/evaluator wrappers so its `.d.ts` matches the new `Wrapper` arity.

## Workflow result changes

New workflows return their declared value directly instead of an internal Output wrapper. Activity results are also no longer wrapped. Code should consume the declared return value without reading a nested `.output`.

Trace destinations now live in Temporal memo and use a flat shape:

```json theme={null}
{
  "trace": {
    "local": "/path/to/trace.json",
    "remote": "s3://trace-bucket/path/to/trace.json"
  }
}
```

The previous `trace.destinations` object is removed. API and CLI JSON results created by v0.11.0 workers include `v: "2"`, and failures expose a structured `error` object instead of only an error string:

```json theme={null}
{
  "v": "2",
  "status": "failed",
  "output": null,
  "trace": null,
  "error": {
    "name": "ValidationError",
    "message": "companyDomain is required"
  }
}
```

v2 also drops legacy `errorDetails.retryable`. That flag was after-the-fact Temporal metadata on a link in the failure chain (whether that failure had been marked retryable while running), not a signal that the API or client should retry — by the time a result is returned the execution is already terminal. The structured `error` object carries the failure content instead.

`POST /workflow/run` no longer hardcodes `status` to only `completed` or `failed`. It now uses the same path as the other result builders (`/workflow/.../result`, list/status, and related endpoints): Temporal's terminal execution status, formatted for the API (`cancelled`, `terminated`, `timed_out`, and so on). If you treated every unsuccessful `/run` as `status === "failed"`, broaden that check — cancellation used to appear as `failed` there even though result endpoints already returned the real status.

The API continues to return the legacy unversioned shape for workflows started by older workers. Consumers that can read results from both versions should branch on `result.v === "2"`.

## Workflow and activity error handling

v0.11.0 changes how the workflow and activity interceptors classify failures. The goal is to follow Temporal's model: only explicit fatal failures complete the workflow run; other workflow code bugs retry the Workflow Task.

### Workflow interceptor

In v0.10.x, **root** workflows attached trace metadata to every thrown error after `ContinueAsNew` / cancellation handling, and the interceptor converted any error carrying that metadata into an `ApplicationFailure`, so the workflow run failed. **Child** workflows did not attach that metadata, so those errors were rethrown unchanged. Cancellation always rethrew before that wrap, on both root and child. v0.11.0 replaces the blanket root wrap with explicit classification:

| Error                                                                                  | v0.10.x                                                                                                                                                                     | v0.11.0                                                                                                                                                                                                                |
| -------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ContinueAsNew`                                                                        | Ends the trace as continued-as-new and rethrows                                                                                                                             | Same                                                                                                                                                                                                                   |
| Cancellation (`isCancellation`)                                                        | Sinks the `Error` and rethrows (never wrapped in `ApplicationFailure`)                                                                                                      | Sinks a serialized error (no `stack`) and rethrows                                                                                                                                                                     |
| `FatalError` / `ValidationError`                                                       | Root: sunk, then converted to `ApplicationFailure` (workflow fails). Child: sunk and rethrown. (`ValidationError` did not extend `FatalError` yet.)                         | Sinks the serialized error and throws a non-retryable `ApplicationFailure` with the serialized error in `.details[0].error`. `TransparentFatalError` is unwrapped to its cause first                                   |
| Temporal failures (`ActivityFailure`, `ChildWorkflowFailure`, other `TemporalFailure`) | Root: sunk, then converted to `ApplicationFailure` when trace metadata was attached. Child: sunk and rethrown. (`CancelledFailure` usually matches `isCancellation` first.) | Sinks the serialized error and rethrows unchanged. (`CancelledFailure` still matches `isCancellation` first when applicable.)                                                                                          |
| Other errors (`TypeError`, syntax/`JSON.parse` failures, unexpected throws)            | Root: sunk, then converted to `ApplicationFailure` → workflow **failed**, masking Temporal Workflow Task retries. Child: sunk and rethrown                                  | Rethrown unchanged **without** sinking → the **Workflow Task retries** (or stays open until timeout). The CLI can look "stuck" until the task succeeds, times out, or you throw `FatalError` / an `ApplicationFailure` |

### Activity interceptor

| Error                                 | v0.10.x                                                                                                                                                                                                        | v0.11.0                                                                                                                                                                                                                                                     |
| ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `CompleteAsyncError`                  | Error hook and trace error fired, then rethrown (or wrapped in `ApplicationFailure` when aggregations were present)                                                                                            | Recorded as an async handoff in the trace (not an error). No activity error hook. Rethrown                                                                                                                                                                  |
| Temporal failures (`TemporalFailure`) | Error hook and trace error fired, then rethrown (or wrapped in `ApplicationFailure` when aggregations were present)                                                                                            | Error hook and trace error fire, then rethrown unchanged                                                                                                                                                                                                    |
| `FatalError` / `ValidationError`      | Error hook and trace error fired, then rethrown (or wrapped in `ApplicationFailure` when aggregations were present). Non-retryable mainly via activity `nonRetryableErrorTypes` (defaults included both names) | Converted to `ApplicationFailure` with serialized error in `.details[0].error`. Always `nonRetryable: true` via `instanceof FatalError`. User `nonRetryableErrorTypes` cannot make these retryable. `TransparentFatalError` is unwrapped to its cause first |
| Other errors                          | Error hook and trace error fired, then rethrown (or wrapped in `ApplicationFailure` when aggregations were present). Retry followed the activity retry policy / `nonRetryableErrorTypes`                       | Converted to `ApplicationFailure` with serialized error in `.details[0].error`. `nonRetryable` when the error class name is listed in `activityInfo.retryPolicy.nonRetryableErrorTypes`                                                                     |

`ValidationError` and `TransparentFatalError` both extend `FatalError` in v0.11.0, so they share the fatal rows above. `TransparentFatalError` did not exist in v0.10.x; it is a transport whose `.cause` is used for logs, traces, hooks, and failure details.

### What to do

* Throw `FatalError` (or `ValidationError`) when the workflow or activity must end without Temporal Task retries.
* Expect ordinary bugs in workflow code (for example a bad `JSON.parse`) to retry the Workflow Task instead of failing the run immediately.
* Read structured failures from `ApplicationFailure.details[0].error` (and from API/CLI `error` on result `v: "2"`) instead of relying on a plain message string alone.
* Update monitors that assumed every root-workflow throw became a failed workflow execution.

## Workflow runtime changes

This section covers the workflow execution changes in `@outputai/core` v0.11.0.

### What changed

* Steps, evaluators, and shared activities now use one workflow-scoped runtime dispatcher.
* Shared activities are registered as `<workflow-name>#<activity-name>` instead of `$shared#<activity-name>`.
* Child workflow invocation options are passed as workflow arguments instead of being propagated through Temporal memo.
* A child workflow's definition-level `activityOptions` now override inherited parent options. Invocation-level and step-level options remain more specific.

### Before upgrading running workers

<Warning>
  v0.11.0 is **not replay-compatible** with open v0.10.x workflow executions. Do not deploy v0.10.x and v0.11.0 workers to the same Temporal task queue, and do not replace a v0.10.x worker while any v0.10.x executions are still open on that queue.
</Warning>

The v0.11.0 runtime changes Temporal history commands and attributes on paths that every workflow hits, not only shared activities or child workflows:

* Every workflow run issues an early `upsertMemo` (`ModifyWorkflowProperties`) for `payloadVersion` (and root `traceInfo` when tracing is enabled). v0.10.x histories do not contain that command at the same point.
* Every scheduled activity gets framework-enforced `retry.nonRetryableErrorTypes` (at least `FatalError`), so activity schedule attributes differ from v0.10.x even when the activity type name is unchanged.
* A shared activity previously scheduled as `$shared#send_event` is now scheduled as `lead_enrichment#send_event`.
* A child workflow is now started with its invocation-level activity options in its argument payload instead of inherited through memo.
* Memo shape and usage changed (flat `trace`, resolved `activityOptions`, no legacy result-wrapper metadata).

Temporal replays a workflow's existing history when another worker processes it. If a v0.11.0 worker replays a history produced by v0.10.x, command type or attributes can differ from the recorded events. Temporal then reports a nondeterminism failure.

Treat **all** open v0.10.x executions as unsafe to replay on v0.11.0. There is no supported mixed-version or rolling cutover on the same task queue. An old parent and new child (or the reverse) can also disagree on memo and child-argument contracts even when replay does not fail immediately.

#### Choose a safe rollout strategy

Use one of these strategies before changing the worker version:

1. **Drain the existing task queue.** Stop starting new workflows on the v0.10.x queue, wait for **all** open executions to complete, and then replace the worker.
2. **Move v0.11.0 to a new task queue.** Keep the v0.10.x worker serving its existing queue until that queue is empty, while new executions are routed to a separate v0.11.0 queue.
3. **Restart open executions.** If executions can be safely terminated and restarted from their original input or a checkpoint, restart them after the v0.11.0 cutover rather than letting v0.11.0 replay v0.10.x history.

Do not rely on a normal rolling restart on the same task queue.

#### Update activity-type consumers

Update dashboards, alerts, history processors, and scripts that match the old `$shared` activity prefix.

##### Before

```ts theme={null}
const isSharedActivity = activityType.startsWith( '$shared#' );
```

##### After

Shared and local activities use the same workflow namespace. Match the full workflow activity type when possible:

```ts theme={null}
const isSendEventActivity = activityType === 'lead_enrichment#send_event';
```

If the workflow name is not known:

```ts theme={null}
const activityName = activityType.split( '#' ).at( -1 );
const isSendEventActivity = activityName === 'send_event';
```

### Review child workflow activity options

Activity options are merged from broad defaults to specific overrides. v0.11.0 changes the relative precedence of inherited parent options and the child workflow's definition.

#### v0.10.x precedence

1. Output's default activity options
2. The child workflow's definition-level `options.activityOptions`
3. Activity options inherited from the parent workflow
4. The `activityOptions` passed to this child invocation
5. The called step's own `options.activityOptions`

#### v0.11.0 precedence

1. Output's default activity options
2. Activity options inherited from the parent workflow
3. The child workflow's definition-level `options.activityOptions`
4. The `activityOptions` passed to this child invocation
5. The called step's own `options.activityOptions`

This means a child definition now protects its own retry and timeout defaults from broader settings inherited through the parent.

For example:

```ts theme={null}
const childWorkflow = workflow( {
  name: 'enrich_company',
  options: {
    activityOptions: {
      retry: {
        maximumAttempts: 2
      }
    }
  },
  async fn( input ) {
    // ...
  }
} );
```

If the parent currently has `maximumAttempts: 8`, the child used 8 attempts in v0.10.x. In v0.11.0, the child uses 2 attempts.

#### Preserve a parent-selected override

Pass the policy on the child invocation when the parent must override the child's definition:

```ts theme={null}
await childWorkflow(
  { companyId: input.companyId },
  {
    activityOptions: {
      retry: {
        maximumAttempts: 8
      }
    }
  }
);
```

Invocation-level options remain more specific than inherited parent options and the child definition.

Alternatively, remove the overlapping option from the child definition when the child should always inherit that field from its parent.

<Note>
  Step-level `options.activityOptions` still have final precedence. Review step definitions as well as parent and child workflow definitions when calculating the effective policy.
</Note>

### Workflow runtime checklist

* Inventory **all** open v0.10.x executions on queues you plan to upgrade — assume none are safe to replay on v0.11.0.
* Drain the old task queue, keep a v0.10.x worker until that queue is empty, route new work to a v0.11.0 queue, or restart open executions before the cutover.
* Do not run v0.10.x and v0.11.0 workers on the same Temporal task queue.
* Update dashboards and history tooling that match `$shared#<activity-name>`.
* Audit parent and child workflows that configure the same retry or timeout fields.
* Pass `activityOptions` on child invocations where the parent must override the child's definition.
* Update tests that assert inherited parent options override child definition options.

## Temporal TypeScript SDK version

Output now ships the Temporal TypeScript SDK (`@temporalio/*`) at **v1.20.3** (previously **v1.17.0**). Re-test any project code that imports `@temporalio/*` directly against that version.

## CLI: `workflow test_eval` renamed to `workflow test`

The workflow evaluation command id is now `workflow:test`. The previous primary id `workflow:test_eval` is removed (it was only the filename-derived id; published docs already used `output workflow test`).

```bash theme={null}
# Before
npx output workflow test_eval <workflowName>

# After
npx output workflow test <workflowName>
```

`output workflow test` continues to work the same way; only the `test_eval` name stops resolving.

### CLI rename checklist

* Replace any scripts, CI steps, or aliases that call `output workflow test_eval` with `output workflow test`.
* No change if you already use `output workflow test`.

## LLM permanent errors and schema-mismatch messages

`@outputai/llm` still maps non-retryable AI SDK failures so Temporal does not retry them, but the transport and some messages changed. This can break log queries, monitors, or tests that matched the old strings or `FatalError` name.

| Case                                                                                                                                          | Before (v0.10.x)                                                                                       | After (v0.11.0)                                                                                               |
| --------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------- |
| Non-retryable AI SDK / API failures (`APICallError` with `isRetryable: false`, `InvalidArgumentError`, `NoSuchModelError`, …)                 | Wrapped in `FatalError` with message `AI-SDK fatal error…: <original>`                                 | Wrapped in `TransparentFatalError`; logs, traces, hooks, and result `error` use the **original** AI SDK error |
| `NoObjectGeneratedError` whose message includes `No object generated: response did not match schema.` and has a `ZodError` in the cause chain | Rebuilt as a new `NoObjectGeneratedError` with `First issue is "<message>" at path [<path>].` appended | Same `NoObjectGeneratedError` returned unchanged (no message rewrite). Still not mapped to a fatal transport  |

`NoObjectGeneratedError` was not turned into `FatalError` in v0.10.x either; only the optional message enrichment was removed.

### Migration steps

* Prefer matching AI SDK error names/types (or structured result `error`) instead of `FatalError` / the `AI-SDK fatal error` prefix.
* If you parsed the appended `First issue is … at path […]` suffix, read issues from the error cause chain (or Zod error) instead.

## Prompt Liquid rendering is strict

`@outputai/llm` now renders `.prompt` files with Liquid `strictVariables: true` and `strictFilters: true` (plus `lenientIf: true`).

### What changed

| Case                                       | Before (v0.10.0)                | After (v0.11.0)                           |
| ------------------------------------------ | ------------------------------- | ----------------------------------------- |
| Undefined variable (`{{ missing }}`)       | Rendered as an empty string     | Throws a non-retryable `FatalError`       |
| Unknown filter (`{{ x \| not_a_filter }}`) | Rendered without failing closed | Throws a non-retryable `FatalError`       |
| Falsy / missing values in `{% if %}`       | Omitted / falsey as usual       | Unchanged (`lenientIf` still allows this) |

### Migration steps

1. Audit `.prompt` files for placeholders that are not always passed in `variables`.
2. Pass every referenced variable (or remove / guard the placeholder). Prefer `{% if var %}` / `| default:` only when the variable is still provided or you intentionally rely on `lenientIf` conditionals.
3. Replace custom or misspelled filters with [supported Liquid filters](https://liquidjs.com/filters/overview.html).
4. Expect render failures to surface as `FatalError` (no Temporal retry) — fix the prompt or inputs rather than catching and retrying.

### Prompt strictness checklist

* Grep prompts for `{{` and confirm each name is supplied by callers.
* Run critical workflows once after upgrade and watch for `Prompt "…" could not be rendered` fatals.
* Update any docs or tests that assumed empty-string rendering for missing variables.
