Repository navigation
feat(tasks): validate task payloads and return values at runtime - #95
Merged
Merged
Conversation
psteinroe
force-pushed
the
feat/runtime-validation
branch
from
October 20, 2026 13:19
2daecdd to
993144b
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Workers now validate every execution of a task defined with
defineTask()against its Standard Schemas (finding C25 / O5-06, owner decision D1).conductor.invoke,ctx.invoke,signalWithStart,pgconductor.invoke()from SQL, dead-letter deliveries). The handler receives the schema's output, so transforms, defaults and stripped keys apply.permanently_failed, so it is not retried. The error lists the issues, e.g.Invalid payload for task "charge": amount: Too small: expected number to be >0. Like any terminal failure, this fails waiting parents and goes to a configured dead-letter queue.DefineTask+fromUnion) have no runtime schema and are not validated.Why
Before this change, schemas only provided types.
createTasknever passed them toTask, so a payload that type-checked but broke a refinement (amount: -1againstz.number().positive()) reached the handler, and SQL invocations were never checked. The check has to happen in the worker because executions don't all go through the TypeScript invoke API.How
createTasklooks up the matching definition (name + queue) inTaskSchemasand hands its schemas toTask(Task.schemas).lib/validate-schema.tsgainsparseSchema, which returns either the issues or the parsed value and does not throw.validateSchema(metadata, events) now uses it.parseOrFailturns issues intopermanently_failedand a thrown validator intofailed. The single path validates inside the existingtry. The batch path validates first and runs the batch with only the valid members (executeBatchTask→executeBatch).conductor.signalWithStart/ctx.signalWithStartwithoutinputnow start the execution with{}instead ofnull, likectx.invokewithout a payload. Anullpayload would fail any object schema;signals.test.ts(from fix(retries): make retried writes from running executions replay-safe #90) starts a session this way.Return value decision
An invalid return value fails permanently, the same as an invalid payload. It's a contract violation by the handler's code, and a retry replays memoized steps, so it would almost always reproduce the same output.
Tests
tests/integration/task-validation.test.ts: refinements, SQL-originated invocations (wrong type, missing payload, valid), transforms and stripped keys, schema defaults on SQL payloads, an async Standard Schema validator, invalid return values, a throwing validator (retried), a batch with one invalid payload and one invalid return value, and type-only definitions. Onmain, 8 of the 9 tests fail; the type-only test passes.just lint,just format,pnpm typecheckand the fullpnpm testpass (538 tests).Docs
defining-tasks.md: new "Runtime Validation" section.batching.md: per-item validation.dead-letter-queue.md: a destination with a payload schema validates the source payload too.AGENTS.md: worker bullet.Follow-ups (out of scope)
conductor.emit/ctx.emit. Events emitted from SQL (emit_event()) reach triggered tasks unvalidated.timeoutMsor cancellation: payload validation runs before the handler race and return validation after it.