@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 themorgan 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 thehttp level regardless of outcome. Now the level is chosen from the response status:
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.
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.
responseTime is now an integer
responseTime is now whole milliseconds (measured with Date.now()) rather than a sub-millisecond float.
Before and after
A successful request, before (v0.10.0):http:
error, with the renamed field:
Migration steps
Update alerts and level-based filters
If you alert or filter on winston levels, audit those rules. HTTP 4xx now arrives atwarn 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.
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 aterror. - Rename
statustostatusCodein log queries, facets, and dashboards; in Datadog, add a remapper fromstatusCodetohttp.status_code. - Replace any match on the constant message
"HTTP request"with a structural field or the new summary format. - Confirm
responseTimeconsumers 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:requestcost:http:requestcost:llm:request- Custom events
onWorkflowStart, onActivityEnd, and onError remain flat.
Before
After
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:
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.
Workflow error hooks receive serialized objects
Workflow errors are serialized before crossing Temporal’s workflow sandbox boundary. Theerror field passed to onWorkflowError and workflow-sourced onError handlers is now a plain error-like object rather than an Error instance.
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>withExternalHookPayload<T>. - Replace
OnHookEnvelopewithActivityPayloadBase. - Replace
ActivityHookPayloadwithActivityPayloadBase.
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
After
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:
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 fromevent.payload. - Guard
event.payloadbefore accessing its fields. - Keep framework fields such as
eventId,workflowDetails, andactivityInfoat the top level. - Remove reads of
aggregationsfrom activity lifecycle and error hooks. - Treat workflow hook errors as serialized error-like objects rather than
Errorinstances. - Filter
outputActivityKind === 'internal_step'in activity lifecycle hooks when internal activities should remain hidden. - Remove assumptions that
onErrorreports$catalogworkflow failures.
HTTP client changes
@outputai/http now exposes two explicitly named, instrumented clients:
outputFetchfor Fetch-compatible requests.createKyClientfor a Ky client backed byoutputFetch.
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:
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:
For example:
ky and undici from @outputai/http.
Update Ky v2 options
Replace Ky’sprefixUrl option with prefix:
Options type.
See Ky’s official 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 1beforeRequest, beforeError, and afterResponse callbacks commonly used positional arguments.
Replace custom wrappers around the old fetch export
If your project created its own Ky factory solely to inject Output’s oldfetch, use createKyClient directly:
createKyClient.
Update direct Fetch usage
Rename direct imports: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:
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:
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.outputFetchandcreateKyClientcontinue to use anEnvHttpProxyAgentand honor standardHTTP_PROXY,HTTPS_PROXY, andNO_PROXYvariables.- For direct npm Undici calls, pass an
undici.EnvHttpProxyAgentor 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=1and set the standard proxy variables.
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/httpand confirm the lockfile resolved Ky 2 and Undici 8. - Rename
fetchandhttpClient. - Update error and option types to the
kyandundicinamespaces. - Replace
prefixUrlwithprefix. - 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:inputSchemaparsing happens beforefn, so the handler receives parsed input.outputSchemaparsing happens afterfn, so workflow and step callers receive parsed output.- Zod coercions, transforms, defaults, and object-key handling now affect runtime values.
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:
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
inputSchemaoroutputSchemamust be deterministic (noDate.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 insidefnwhen 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 intocontinueAsNew(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.coerceand.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 theinputSchema and outputSchema of every workflow, step, and evaluator:
- Update handlers and callers to use the parsed Zod input and output types (
z.inferin handlers,z.inputat 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
inputSchematransforms are idempotent if youcontinueAsNewwith values that will be parsed again, or pass wire-format input instead. - Republish any package that exports workflow/step/evaluator wrappers so its
.d.tsmatches the newWrapperarity.
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:
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:
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 afterContinueAsNew / 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:
Activity interceptor
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(orValidationError) 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/CLIerroron resultv: "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
activityOptionsnow override inherited parent options. Invocation-level and step-level options remain more specific.
Before upgrading running workers
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) forpayloadVersion(and roottraceInfowhen 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 leastFatalError), 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_eventis now scheduled aslead_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, resolvedactivityOptions, no legacy result-wrapper metadata).
Choose a safe rollout strategy
Use one of these strategies before changing the worker version:- 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.
- 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.
- 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.
Update activity-type consumers
Update dashboards, alerts, history processors, and scripts that match the old$shared activity prefix.
Before
After
Shared and local activities use the same workflow namespace. Match the full workflow activity type when possible: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
- Output’s default activity options
- The child workflow’s definition-level
options.activityOptions - Activity options inherited from the parent workflow
- The
activityOptionspassed to this child invocation - The called step’s own
options.activityOptions
v0.11.0 precedence
- Output’s default activity options
- Activity options inherited from the parent workflow
- The child workflow’s definition-level
options.activityOptions - The
activityOptionspassed to this child invocation - The called step’s own
options.activityOptions
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:Step-level
options.activityOptions still have final precedence. Review step definitions as well as parent and child workflow definitions when calculating the effective policy.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
activityOptionson 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).
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_evalwithoutput 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.
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 ofFatalError/ theAI-SDK fatal errorprefix. - 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
Migration steps
- Audit
.promptfiles for placeholders that are not always passed invariables. - Pass every referenced variable (or remove / guard the placeholder). Prefer
{% if var %}/| default:only when the variable is still provided or you intentionally rely onlenientIfconditionals. - Replace custom or misspelled filters with supported Liquid filters.
- 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 renderedfatals. - Update any docs or tests that assumed empty-string rendering for missing variables.