Conversation
Capture the planning documents that drive the v1 cleanup so the branch has the source of truth for sequencing and decisions. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Mechanical rename of execution-thread terminology from "path" to "branch" across the public API, internal engine, tests, and examples: - Types: Path→Branch, PathState→BranchState, PathLocalState→ BranchLocalState, PathSnapshot→BranchSnapshot, PathOptions→ BranchOptions, PathSpec→BranchSpec, PathExecutionEvent→ BranchExecutionEvent. - IDs and methods: PathID→BranchID, PausePath→PauseBranch, UnpausePath→UnpauseBranch, GetPathID→GetBranchID, PausePathInCheckpoint→PauseBranchInCheckpoint (and unpause). - Errors: ErrPathNotFound→ErrBranchNotFound. - Fields: Edge.Path→Edge.BranchName, JoinConfig.Paths→ JoinConfig.Branches, JoinConfig.PathMappings→ JoinConfig.BranchMappings, Output.Path→Output.Branch, Checkpoint.PathStates→BranchStates, PathCounter→BranchCounter. - Callbacks: BeforePathExecution→BeforeBranchExecution and friends. - Deletes Workflow.Path(), Workflow.path field, Options.Path. These were never load-bearing. - Renames files path*.go → branch*.go and examples/join_paths → examples/join_branches. The word "path" survives only as English prose for state dot-notation (state.foo.bar) and genuine filesystem paths (filepath, checkpointer_file.go, file_activity). Tests + go vet green. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Hide orchestration plumbing types that consumers should never see. Opaque Branch, unexported internal snapshot/request types, deleted the undocumented WorkflowFormatter. Unexport: - Branch (struct), branchOptions, branchSpec, branchSnapshot - activityExecutor (internal side interface) - waitRequest, joinRequest, pauseRequest - executionState, executionAdapter - patch, patchOptions, newPatch, generatePatches, applyPatches - newSignalWait, newSleepWait, isWaitUnwind (asWaitUnwind added as the *waitUnwindError extractor for internal error handling) - newBranch Keep exported (required by the checkpoint wire format or by consumer tests until PR6 lands FakeContext): - BranchState, JoinState, WaitState, WaitKind - BranchLocalState, NewBranchLocalState - NewContext, ExecutionContextOptions Delete WorkflowFormatter entirely: no consumers, undocumented, and ExecutionCallbacks covers the same use case. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
workflow.New now runs structural validation eagerly and fails fast with a *ValidationError containing every problem found. Validation is independent of the activity registry and script compiler — binding-level checks ship in PR5. New: - Options.StartAt names the initial step (default: Steps[0]). - Retry / catch modifier rejection on non-activity, non-wait_signal steps. - RetryConfig sanity (MaxRetries >= 0, BaseDelay <= MaxDelay, BackoffRate >= 0). - Duplicate branch name detection now surfaces as a structured ValidationProblem instead of an ad-hoc fmt error. - ValidationProblem carries an Err sentinel; *ValidationError implements Is so errors.Is(err, ErrDuplicateStepName) etc. works. Removed: - Unreachable-step check. Per plan: warn-but-don't-error is better v1 posture. We'll reintroduce it as a soft warning later if consumers ask. New sentinels: ErrDuplicateStepName, ErrEmptyStepName, ErrUnknownStartStep, ErrUnknownEdgeTarget, ErrUnknownCatchTarget, ErrUnknownJoinBranch, ErrInvalidStepKind, ErrInvalidModifier, ErrInvalidRetryConfig, ErrInvalidSleepConfig, ErrInvalidWaitConfig, ErrReservedBranchName, ErrDuplicateBranchName. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The biggest reshape in the v1 cleanup. Every public entry point
into the engine now looks like post-v1 code.
NewExecution signature:
NewExecution(wf *Workflow, reg *ActivityRegistry,
opts ...ExecutionOption) (*Execution, error)
Activity registration:
reg := workflow.NewActivityRegistry()
reg.MustRegister(myActivity)
reg.MustRegister(anotherActivity)
Execution options (functional):
WithInputs, WithCheckpointer, WithSignalStore, WithLogger,
WithExecutionID, WithExecutionCallbacks, WithStepProgressStore,
WithActivityLogger, WithScriptCompiler.
Run method collapse: Execution now exposes exactly one run
method, with resume as an option:
exec.Execute(ctx)
exec.Execute(ctx, workflow.ResumeFrom(priorID))
Deleted Run, Resume, RunOrResume, ExecuteOrResume. The
Execute(ctx, ResumeFrom(id)) form silently falls back to a fresh
run when no checkpoint is found, matching the old RunOrResume
semantics.
Runner options also move to functional form:
runner := workflow.NewRunner(
workflow.WithRunnerLogger(l),
workflow.WithDefaultTimeout(5*time.Minute),
)
result, err := runner.Run(ctx, exec,
workflow.WithHeartbeat(hb),
workflow.WithCompletionHook(hook),
workflow.WithRunTimeout(30*time.Second),
workflow.WithResumeFrom(priorID),
)
Activity function renames (http.HandlerFunc style):
NewActivityFunction → ActivityFunc
NewTypedActivityFunction → TypedActivityFunc
Internal struct types activityFunc / typedActivityFunc are now
unexported.
ActivityRegistry is an opaque struct with Register / MustRegister
/ Get / Names — the old type alias
`map[string]Activity` is gone. Register returns
ErrDuplicateActivity on repeat names.
Also fixes the runner's completion-hook behavior: FollowUps are
attached even when the hook returns an error (logged separately).
Every example, test, and consumer site is updated to the new
shape. Old tests that exercised the Run/Resume/RunOrResume error
return contract were rewritten to match the new
(*ExecutionResult, error) shape.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds phase-2 validation that runs after NewExecution binds the
ActivityRegistry and script.Compiler:
- Unknown activity references
- Parameter templates ("${...}") and $() expressions compile
- Edge condition expressions compile
- WaitSignalConfig.Topic templates compile
- Store fields (Step.Store, WaitSignalConfig.Store, CatchConfig.Store,
Output.Variable) reject "state." prefix
- Warn (do not error) when WaitSignal is used without a SignalStore
New sentinels: ErrUnknownActivity, ErrInvalidTemplate,
ErrInvalidExpression, ErrInvalidStorePath. All problems collected into
*ValidationError so errors.Is works.
Updates tests and examples to use bare variable names for Store fields.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fold VariableContainer and the SignalAware / ActivityHistoryAware /
ProgressReporter side-interfaces into Context, and rename the
GetFoo/ListFoo accessors to property-style.
Context now exposes:
Inputs() Inputs
Set/Get/Delete/Keys (variables)
Logger, Compiler, BranchID, StepName
Wait(topic, timeout)
History() *History
ReportProgress(detail)
Package-level helpers workflow.Wait, workflow.ActivityHistory,
workflow.ReportProgress, InputsFromContext, and VariablesFromContext
are gone. Activity code calls the Context methods directly.
Inputs is a named struct (map wrapper) with Get/Keys/Len/ToMap so we
can grow typed accessors later without reopening the interface.
New workflowtest.FakeContext lets consumer tests unit-test activities
without spinning up a real Execution. NewFakeContext takes a
FakeContextOptions and returns a fully usable workflow.Context. Two
small helper constructors (workflow.NewInputsForTest,
workflow.NewHistoryForTest) exist so FakeContext can build these
values without reaching into package internals.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add SchemaVersion to Checkpoint with a defined forward-compatibility contract: readers reject checkpoints with a version newer than the library, so rolling back after a wire-format bump fails loudly instead of silently dropping fields. Change Status from bare string to the ExecutionStatus typed constant so the JSON shape is enforced by the type system. Rename lingering "path_states" / "path_counter" JSON keys to "branch_states" / "branch_counter" to match the PR1 rename. Add an AtomicCheckpointer optional side interface: backends with transactional primitives (Postgres row-lock, Redis CAS) can implement AtomicUpdate to close the load-modify-write race that bare PauseBranchInCheckpoint / UnpauseBranchInCheckpoint would otherwise open when a host process is concurrently writing the same execution. MemoryCheckpointer implements it; mutatePauseInCheckpoint prefers it when available and falls back to load-modify-write otherwise. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
One template syntax, contextual type inference. Delete $(...) parsing
entirely; ${...} is the only form. When a parameter value is a single
${...} covering the whole trimmed string, the result preserves its
native type; otherwise it is interpolated as a string. Conditions and
each.Items are raw expressions.
- script/eval.go: parse only ${...}; single-expression templates
preserve typed values, interpolated templates return strings.
- script/eval_test.go: cover int/bool/whitespace single-expression
and EvalString stringification.
- branch.go: drop $(...) handling; conditions and each.Items are raw
expressions; parameter templates unified on ${...}.
- validate.go: drop $(...) branches in parameter/condition/topic
validation.
- branch_test.go, coverage_test.go, validate_test.go: rewrite to use
${...} templates.
- examples/{child_workflows,branching,simple,structured_result}:
migrate to ${...}.
- step.go, README.md, llms.txt, CLAUDE.md, examples/join_branches/
README.md: doc/comment cleanup removing $(...) references.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Reorganise the activities/ tree by guarantee level. Core safe
activities stay in activities/. HTTP moves to activities/httpx/.
Host-touching activities (shell, file) move to activities/contrib/.
Delete the in-process wait activity — durable Sleep replaces it.
- activities/contrib/{shell,file}_activity.go: moved + repackaged
- activities/httpx/http_activity.go: moved + repackaged
- activities/{contrib,httpx}/helpers_test.go: per-package newTestContext
- activities/wait_activity{,_test}.go: deleted
- activities/print_activity.go: PrintActivity now wraps an io.Writer.
NewPrintActivity() defaults to os.Stdout; NewPrintActivityTo(w)
injects a custom writer for tests and embedded uses.
- activities/contrib/shell_activity.go: ShellInput.Timeout becomes
time.Duration (was float64 seconds).
- activities/httpx/http_activity.go: HTTPInput.Timeout becomes
time.Duration; default of 30s preserved.
- activities/child_workflow_activity.go: ChildWorkflowInput.Timeout
becomes time.Duration; the float→Duration conversion in Execute is
gone.
- cmd/workflow/main.go: import contrib + httpx, drop the wait
activity from the registry.
- examples/simple/main.go: drop the wait step (it was just a sleep
between loop iterations) and the wait activity registration.
- README.md, llms.txt: doc updates for the new sub-packages,
PrintActivity io.Writer story, and rename of NewActivityFunction →
ActivityFunc / NewTypedActivityFunction → TypedActivityFunc that
PR4 made but the docs still referenced. activity_functions_test.go
and typed_activity_example_test.go: matching test/comment renames.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Per decision 1.15: keep child workflows in the public API and fix the concrete bugs. - ChildWorkflowSpec.Sync deleted. The ExecuteSync/ExecuteAsync split on the executor interface is the source of truth; flagging on the spec was redundant and confusing. - ChildWorkflowResult.Error dropped. The execution error is the second return value of ExecuteSync/GetResult — duplicating it on the struct led to two sources of truth. - ChildWorkflowExecutorOptions.CleanupTimeout added. Default 1h (was a hardcoded 5min); zero means use default; negative disables eviction entirely. Replaces the magic 5-minute sleep. - ExecuteAsync godoc spells out the async-vs-checkpoint contract: in-process only, dies with the process, parent loses the handle on restart. TODO(v1.1) for durable async-child handles. - activities/child_workflow_activity.go: drop the Sync field from ChildWorkflowInput and the executeSync/executeAsync branching. The bundled `workflow.child` activity is sync-only; consumers needing fire-and-forget build their own activity wrapping executor.ExecuteAsync. - activities/child_workflow_activity_test.go: drop the async path test, drop "sync": true from the remaining cases. - examples/child_workflows/main.go: drop "sync": true, switch timeouts to time.Duration literals. - examples/child_workflows/README.md: rewrite to document the wait-for-completion pattern (which is just `workflow.child` with Step.Store) and the async durability caveat. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- ClassifyError no longer substring-matches "timeout" in error messages, and no longer routes context.Canceled into the timeout bucket. Real timeouts must wrap context.DeadlineExceeded or workflow.ErrWaitTimeout. Substring matching of "timeout" was a classic surprise (any error string containing the word would unexpectedly route through timeout catch handlers). - ErrorTypeAll godoc now spells out the fatal-error escape valve: ErrorTypeAll matches everything except ErrorTypeFatal, which is matchable only by an explicit ErrorTypeFatal pattern. - ErrorTypeTimeout godoc spells out the new "real timeouts only" classification rule. - WorkflowError.Details godoc spells out the non-roundtrip contract: Details is any so consumers can attach structure, but Checkpoint.Error is a flat string and Details is dropped on resume. Consumers needing persistent structured details should wrap a custom error type instead. - WorkflowError.Error() now prefixes "workflow: " for consistency with the rest of the package's error strings. - All root-package error sentinels (ErrNoCheckpoint, ErrAlreadyStarted, ErrNilExecution, ErrInvalidHeartbeatInterval, ErrNilHeartbeatFunc) and workflow.New's name/steps fmt.Errorf calls now use the "workflow: " prefix. - errors_test.go and workflow_test.go updated for the new error strings. Note: the runner.go FollowUps-on-hook-error fix and the completion_hook.go godoc were already in place from earlier work, so this PR is godoc + classification only on those files. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add convenience accessors that consumers will actually reach for so they don't have to write the same map-lookup-and-type-assert boilerplate at every call site. Outputs: - ExecutionResult.Output(key) — raw lookup with presence flag. - ExecutionResult.OutputString(key) - ExecutionResult.OutputInt(key) — accepts int/int32/int64/float32/ float64 so JSON-decoded numbers (which arrive as float64) work. - ExecutionResult.OutputBool(key) - workflow.OutputAs[T](r, key) — package-level generic for arbitrary types, including custom structs. Suspension: - ExecutionResult.WaitReason() — dominant SuspensionReason or "". - ExecutionResult.Topics() — union of waited-on signal topics or nil. - ExecutionResult.NextWakeAt() — earliest wall-clock deadline + ok. All accessors are nil-safe on the receiver. Tests cover happy paths, type mismatches, missing keys, and nil receivers. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The library should be recommendable to someone reading the README, the MIGRATION doc, and the suspension model doc cold. Update the docs to reflect the v1 surface and contracts. Long-form docs: - README.md — quick example uses Runner and the v1 functional-options constructors. Adds a "Going to production" section linking to the new docs. - MIGRATION.md (new) — every breaking change between pre-v1 and v1 with before/after snippets, organised by PR. - docs/suspension.md (new) — the consolidated suspension model. The three reasons table, lifecycle diagram, replay-safety contract, RecordOrReplay shape, scheduling-resume recipe, dominant-reason precedence rule. - docs/production_checklist.md (new) — punch list for taking the library to production: storage, execution, activity authoring, suspension/resume, observability, worker hygiene. - llms.txt — reflects v1 API throughout. Replaces every stale Path/path reference with Branch, every ExecutionOptions struct example with the functional-options constructor, every execution.Run/Resume/RunOrResume with exec.Execute, every GetVariable/SetVariable with Get/Set, every RunnerConfig/RunOptions with NewRunner/Run options, and adds the new ExecutionResult helpers (OutputString/OutputInt/OutputBool/OutputAs/WaitReason/ Topics/NextWakeAt) to the Execution section. Godoc: - checkpoint.go — Checkpoint godoc gets a "Load-bearing fields" section that explicitly lists BranchState.Variables / Wait / PauseRequested / ActivityHistory as round-trip-required. - step.go — Step godoc documents the now-validated "exactly one kind" rule and the modifier-field restrictions. - context.go — Context.Wait godoc spells out the behavior, the replay-safety contract, the deadline rules, and the custom-Context-implementer contract. Includes the canonical RecordOrReplay shape. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add runnable end-to-end examples for the three suspension primitives (signal wait, durable sleep, operator pause) so consumers have a working template for the full suspend + resume cycle. Fix stale references in llms.txt, docs/suspension.md, and context.go godoc that still showed the pre-fold package-level helpers (workflow.Wait, workflow.ActivityHistory, workflow.ReportProgress, workflow.RecordOrReplay). All four were folded into Context methods in v1; the docs now match the real API. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Finish the PR1 rename in the places it missed: JSON field waiting_path_id → waiting_branch_id, the Go field WaitingPathID → WaitingBranchID, the JSON tag path_id → branch_id on ActivityLogEntry.BranchID, and the slog keys path_id/waiting_path → branch_id/waiting_branch across execution.go and branch.go. Also renames local vars and comments that still said "path". Wire format impact: ActivityLogEntry and JoinState JSON both change key names. This matches the pre-v1 MIGRATION guidance that checkpoints written before v1 cannot be loaded — re-run any in-flight executions rather than trying to convert old data. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
expr is now the default ScriptCompiler, so the integration example shows redundant wiring and the rest are pure expr-language demos that belong in the expr repo. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (20)
📝 WalkthroughWalkthroughThis PR migrates the runtime model from "paths" to "branches", consolidates execution APIs to registry+functional options, adds checkpoint schema versioning, moves activities into httpx/contrib, standardizes durations and Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant Runner
participant Execution
participant ActivityRegistry
participant Checkpointer
User->>Runner: Run(ctx, exec, WithResumeFrom?/options...)
Runner->>Execution: Execute(ctx, execOptions...)
Execution->>ActivityRegistry: Lookup activity by name
ActivityRegistry-->>Execution: Activity
Execution->>Activity: Execute activity with BranchLocalState
Activity-->>Execution: Activity result
Execution->>Checkpointer: SaveCheckpoint(SchemaVersion=CheckpointSchemaVersion)
Checkpointer-->>Execution: ack
Execution-->>Runner: ExecutionResult
Runner-->>User: result/status/outputs
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
Poem
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
|
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 17
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (8)
cmd/workflow/main.go (2)
134-139:⚠️ Potential issue | 🔴 CriticalPropagate
ExecutionResult, not justerr.
Executecan returnerr == nilwithresult.Status == failed. Dropping the result here meansshowExecutionResultsprints "Execution successful!" and exits 0 for failed workflows.Suggested fix
- _, err = execution.Execute(ctx) + result, err := execution.Execute(ctx) duration := time.Since(startTime) // Show execution results - showExecutionResults(execution, err, duration, config) + showExecutionResults(execution, result, err, duration, config)-func showExecutionResults(execution *workflow.Execution, err error, duration time.Duration, config *Config) { +func showExecutionResults(execution *workflow.Execution, result *workflow.ExecutionResult, err error, duration time.Duration, config *Config) { status := execution.Status() @@ - if err != nil { + if err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) if status != workflow.ExecutionStatusCompleted { os.Exit(1) } - } else { + } else if result != nil && result.Status != workflow.ExecutionStatusCompleted { + if result.Error != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", result.Error) + } + os.Exit(1) + } else { info("Execution successful!") }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@cmd/workflow/main.go` around lines 134 - 139, The code currently discards the ExecutionResult returned by execution.Execute and only checks err, which can be nil while result.Status indicates failure; capture the returned ExecutionResult (e.g., result := ...) from Execute and pass that result into showExecutionResults instead of only passing execution and err so showExecutionResults can inspect result.Status; update the call sites and any function signature of showExecutionResults if needed to accept the ExecutionResult (or add an overload) and use result for final success/failure determination.
279-289:⚠️ Potential issue | 🟠 MajorCLI help and registered built-ins drifted apart.
createActivityRegistryno longer registers a wait activity, but-helpstill advertiseswaitas supported. That sends users toward a runtime failure from the first example they copy. Either re-add the activity here or remove it from the usage text.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@cmd/workflow/main.go` around lines 279 - 289, The CLI help advertises a "wait" activity but createActivityRegistry no longer registers it; fix by restoring the built-in or removing it from the usage text: either add the Wait activity back into createActivityRegistry (e.g., insert the wait activity constructor into the activityList alongside activities.NewPrintActivity(), activities.NewTimeActivity(), etc.) or remove "wait" from the CLI usage/help string where activities are listed (search for the help/usage text that mentions "wait"); ensure the symbol names createActivityRegistry and the wait activity constructor you add/remove are updated consistently so help and the registered activities no longer drift.child_workflow.go (1)
285-305:⚠️ Potential issue | 🟠 MajorDon't drop the terminal async execution result.
This goroutine ignores the values returned by
execution.Execute(execCtx). Downstream,GetResult()can only synthesize a generic"child workflow execution failed"and cannot surface the real failure cause or terminal timing for async callers. Store the terminalExecutionResult/error alongside the handle and read that back inGetResult().Also applies to: 343-355
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@child_workflow.go` around lines 285 - 305, The goroutine that calls execution.Execute(execCtx) currently discards its returned ExecutionResult/error; capture that terminal result and store it together with the handle in the asyncExecutions map so GetResult() can return the real outcome and timing. Specifically, change the asyncExecutions value to include a field for the terminal result/error (or add a separate results map keyed by execution.ID()), assign the result of execution.Execute(execCtx) into that storage before deleting the entry, and ensure the existing cleanup logic (the time.Sleep cleanup goroutine) does not delete the stored result before GetResult() reads it; update GetResult() to consult the stored ExecutionResult/error for the given execution.ID() instead of synthesizing a generic message. Ensure uses of asyncExecutionsMtx protect both writing the result and deleting the entry.context.go (1)
264-306:⚠️ Potential issue | 🟠 MajorPreserve arbitrary
Contextimplementations when wrapping timeout/cancel.For any
Contextthat isn't*executionContext, these helpers return a brand-newexecutionContextwith none of the parent's state, inputs, logger, orWaitbehavior. That breaks theContextcontract as soon as callers wrap a custom implementation. Delegate to the parent instead of replacing it.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@context.go` around lines 264 - 306, The helpers WithTimeout and WithCancel currently replace non-*executionContext parents with a fresh executionContext losing the parent's state; instead, after creating ctx and cancel (ctx, cancel := context.WithTimeout(parent, timeout) / context.WithCancel(parent)), return the original parent along with cancel when parent is not a *executionContext so arbitrary Context implementations are preserved (i.e., in WithTimeout and WithCancel, change the final return from &executionContext{Context: ctx} to return parent, cancel).examples/error_handling/main.go (1)
118-126:⚠️ Potential issue | 🟡 MinorCheck the returned execution result before reporting success.
Execute()separates setup/runtime errors from workflow outcome. If the workflow ends inFailed,errcan still benil, so this path can print “completed successfully” for a failed run.💡 Proposed fix
- _, err = execution.Execute(context.Background()) + result, err := execution.Execute(context.Background()) if err != nil { fmt.Printf("Workflow failed: %v\n", err) os.Exit(1) } + if result.Failed() { + fmt.Printf("Workflow failed: %v\n", result.Error) + os.Exit(1) + } fmt.Printf("Workflow completed successfully!\n") - fmt.Printf("Status: %s\n", execution.Status()) - fmt.Printf("Final outputs: %+v\n", execution.GetOutputs()) + fmt.Printf("Status: %s\n", result.Status) + fmt.Printf("Final outputs: %+v\n", result.Outputs)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/error_handling/main.go` around lines 118 - 126, The current code treats a nil error from execution.Execute(...) as success, but Execute can return nil err while the workflow finished with a failed status; check execution.Status() after Execute returns and treat non-success (e.g., "Failed" or not "Completed") as an error case. Update the post-Execute logic around execution.Status() (and optionally execution.GetOutputs()) to print a failure message and call os.Exit(1) when the status indicates failure, otherwise print the success messages as currently done.README.md (1)
55-73:⚠️ Potential issue | 🟠 MajorDeclare
resultas an output before reading it fromExecutionResult.The example stores the activity result in branch state, but
result.OutputString("result")only reads declared workflow outputs. As written, this branch prints nothing even on success.🛠️ Proposed fix
wf, err := workflow.New(workflow.Options{ Name: "demo", Steps: []*workflow.Step{ { Name: "Call My Operation", Activity: "my_operation", Store: "result", Retry: []*workflow.RetryConfig{{MaxRetries: 2}}, Next: []*workflow.Edge{{Step: "Finish"}}, }, { Name: "Finish", Activity: "print", Parameters: map[string]any{ "message": "Workflow completed. Result: ${state.result}", }, }, }, + Outputs: []*workflow.Output{ + {Name: "result", Variable: "result"}, + }, })Also applies to: 98-100
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@README.md` around lines 55 - 73, The example assigns the activity result to branch state via Step.Store but never declares a workflow output, so calls like result.OutputString("result") read nothing; update the workflow definition returned by workflow.New (workflow.Options) to include an Outputs entry declaring "result" and map it to the stored state key (e.g., state.result or the framework's output path) so that ExecutionResult.OutputString("result") can read the value; adjust the same pattern used in the other occurrence (lines ~98-100) to declare the output there as well.examples/branching/main.go (1)
243-248:⚠️ Potential issue | 🟡 MinorUpdate the example summary text.
Item 4 still says “Script activities for calculations”, but this example now uses Go activities throughout. The stale console output is misleading in a v1 migration example.
🛠️ Proposed fix
- fmt.Println("4. Script activities for calculations") + fmt.Println("4. Go activities for calculations")🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/branching/main.go` around lines 243 - 248, Update the example summary strings printed in main (the fmt.Println calls) so item 4 no longer says "Script activities for calculations" and instead reflects that the example uses Go activities (e.g., change to "Go activities for calculations" or similar); locate the fmt.Println lines in main() that print the numbered list (the five fmt.Println calls shown) and modify the fourth entry text accordingly to remove the stale "Script activities" wording.execution_callbacks_test.go (1)
209-217:⚠️ Potential issue | 🟡 MinorAssert the failure hooks explicitly.
This test says it verifies failure callbacks, but the expected event set omits
OnWorkflowExecutionFailure,OnBranchFailure, andOnActivityFailure. It can pass even if those hooks stop firing.🛠️ Proposed fix
- require.Equal(t, 6, len(eventTypes), "Should have 6 callbacks") - require.Equal(t, map[string]bool{ - "BeforeWorkflowExecution": true, - "AfterWorkflowExecution": true, - "BeforeBranchExecution": true, - "AfterBranchExecution": true, - "BeforeActivityExecution": true, - "AfterActivityExecution": true, - }, eventTypes) + require.True(t, eventTypes["BeforeWorkflowExecution"]) + require.True(t, eventTypes["AfterWorkflowExecution"]) + require.True(t, eventTypes["BeforeBranchExecution"]) + require.True(t, eventTypes["AfterBranchExecution"]) + require.True(t, eventTypes["BeforeActivityExecution"]) + require.True(t, eventTypes["AfterActivityExecution"]) + require.True(t, eventTypes["OnWorkflowExecutionFailure"]) + require.True(t, eventTypes["OnBranchFailure"]) + require.True(t, eventTypes["OnActivityFailure"])🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@execution_callbacks_test.go` around lines 209 - 217, The test currently asserts eventTypes contains only the six non-failure hooks (using require.Equal on eventTypes) but omits the failure hooks; update the assertion that checks eventTypes (the map used in the require.Equal) to include "OnWorkflowExecutionFailure", "OnBranchFailure", and "OnActivityFailure" set to true (or replace the single require.Equal with explicit require.True checks for each of those keys) so the test explicitly verifies those failure callbacks are present when callbacks fire (targets: the eventTypes variable and the existing require.Equal/assertion around it).
🧹 Nitpick comments (7)
checkpoint_test.go (1)
81-84: Interface conformance checks could be package-level declarations.The compile-time interface assertions work but are more idiomatically placed as package-level
var _ = ...declarations rather than inside a test function. This is a minor style preference.♻️ Optional: Move to package-level
+var ( + _ workflow.Checkpointer = (*workflowtest.MemoryCheckpointer)(nil) + _ workflow.AtomicCheckpointer = (*workflowtest.MemoryCheckpointer)(nil) +) + func TestAtomicCheckpointerInterface(t *testing.T) { - var _ workflow.Checkpointer = (*workflowtest.MemoryCheckpointer)(nil) - var _ workflow.AtomicCheckpointer = (*workflowtest.MemoryCheckpointer)(nil) + // Interface conformance is checked at package level }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@checkpoint_test.go` around lines 81 - 84, The compile-time interface assertions currently inside TestAtomicCheckpointerInterface should be moved to package-level declarations: replace the two lines in TestAtomicCheckpointerInterface (the var _ checks referencing workflow.Checkpointer, workflow.AtomicCheckpointer and workflowtest.MemoryCheckpointer) with package-level var _ = ... style assertions placed outside any function (e.g., at top of the file), and remove or simplify TestAtomicCheckpointerInterface accordingly so the assertions are evaluated at compile time without being inside a test function.planning/review/v1_implementation_plan.md (1)
300-305: Add language specifier to fenced code block.The code block at line 300 lacks a language specifier, which may affect rendering in some Markdown viewers.
📝 Proposed fix
-``` +```text activities/ # safe, in-process primitives only print, json, fail, time helpers, random helpers activities/httpx/ # http client (safe but I/O) activities/contrib/ # shell, file, anything environment-specific or risky</details> <details> <summary>🤖 Prompt for AI Agents</summary>Verify each finding against the current code and only fix it if needed.
In
@planning/review/v1_implementation_plan.mdaround lines 300 - 305, The fenced
code block containing the directory list (the block starting with triple
backticks above the lines "activities/ # safe, in-process primitives
only" through "activities/contrib/ # shell, file, anything
environment-specific or risky") needs a language specifier to ensure proper
Markdown rendering; update the opening fence fromtotext (or another
appropriate language token) so the block is marked as plain text in the file.</details> </blockquote></details> <details> <summary>branch_join_test.go (1)</summary><blockquote> `66-78`: **Unsafe type assertions may cause test panics.** The type assertions on lines 67-68, 71-72, and 75-77 (e.g., `value.(int)`) don't check the `ok` boolean. If the type doesn't match, these will panic rather than producing a clear test failure. <details> <summary>♻️ Proposed fix to add type safety</summary> ```diff reg.MustRegister(ActivityFunc("double", func(ctx Context, params map[string]any) (any, error) { - value, _ := ctx.Get("value") - return value.(int) * 2, nil + value, ok := ctx.Get("value") + if !ok { + return nil, fmt.Errorf("variable 'value' not found") + } + v, ok := value.(int) + if !ok { + return nil, fmt.Errorf("variable 'value' is not an int") + } + return v * 2, nil })) ``` </details> <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against the current code and only fix it if needed. In `@branch_join_test.go` around lines 66 - 78, The activities "double", "triple", and "sum" use unchecked type assertions (e.g., value.(int), doubled.(int), tripled.(int)) which can panic; update each ActivityFunc handler to safely assert types by capturing the comma-ok result (and check ctx.Get errors) and return a descriptive error if the value is missing or not an int instead of asserting blindly; specifically change the "double" and "triple" handlers to verify that ctx.Get("value") yields an int before using it, and change the "sum" handler to verify that ctx.Get("doubled") and ctx.Get("tripled") yield ints before adding, returning an error when checks fail. ``` </details> </blockquote></details> <details> <summary>planning/review/combined_api_review.md (1)</summary><blockquote> `403-409`: **Add language specifier to fenced code block.** The code block lacks a language specifier. <details> <summary>📝 Proposed fix</summary> ```diff -``` +```text activities/ # safe, in-process, side-effect-free primitives print, time, random, json, fail activities/httpx/ # http client activities/contrib/ # shell, file, anything environment-specific ``` ``` </details> <details> <summary>🤖 Prompt for AI Agents</summary>Verify each finding against the current code and only fix it if needed.
In
@planning/review/combined_api_review.mdaround lines 403 - 409, The fenced
code block showing the activity directory listing (lines containing
"activities/", "activities/httpx/", "activities/contrib/") is missing a language
specifier; update the opening fence to include a language (for example change
totext or ```bash) so the block is rendered with the correct syntax
highlighting and preserved formatting.</details> </blockquote></details> <details> <summary>typed_activity_example_test.go (1)</summary><blockquote> `44-44`: **Consider renaming `BranchID` value to match the new terminology.** Using `"path1"` under `BranchID` is a small terminology mismatch; `"branch1"` would be clearer in v1 examples/tests. <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against the current code and only fix it if needed. In `@typed_activity_example_test.go` at line 44, Rename the BranchID test value to use the new terminology: change the literal assigned to the BranchID field (in the typed_activity_example_test.go example/test) from "path1" to "branch1" so the BranchID value matches v1 naming conventions. ``` </details> </blockquote></details> <details> <summary>docs/suspension.md (1)</summary><blockquote> `89-91`: **Don't freeze internal state layout in the public docs.** Calling out `BranchState.ActivityHistory` here makes an internal storage detail look like supported API. I'd describe this as a per-step replay cache and avoid naming the backing field. <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against the current code and only fix it if needed. In `@docs/suspension.md` around lines 89 - 91, Replace the explicit internal field reference "BranchState.ActivityHistory" in the docs with a description that it is a per-step replay cache (or "per-step history cache") and avoid naming any backing field; update the sentence that currently reads "The `History` cache is per-step: it lives on `BranchState.ActivityHistory` and is cleared when the step advances past the activity." to instead say something like "The history is a per-step replay cache that is cleared when the step advances past the activity" so the docs describe behavior without exposing internal storage names. ``` </details> </blockquote></details> <details> <summary>activity_functions_test.go (1)</summary><blockquote> `79-83`: **Avoid asserting the unexported backing type here.** This locks the test to `*typedActivityFunc[...]`, so harmless adapter refactors will fail the test even if `TypedActivityFunc` still exposes the same type metadata. <details> <summary>♻️ Proposed refactor</summary> ```diff - typedFunc, ok := adapter.Activity().(*typedActivityFunc[Person, string]) + typedFunc, ok := adapter.Activity().(interface { + ParametersType() reflect.Type + ResultType() reflect.Type + }) require.True(t, ok) ``` </details> <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against the current code and only fix it if needed. In `@activity_functions_test.go` around lines 79 - 83, The test is asserting the concrete unexported backing type (*typedActivityFunc[Person, string]) which couples the test to an implementation detail; instead, change the assertion to check that adapter.Activity() satisfies the exported TypedActivityFunc[Person, string] interface (or at minimum exposes the ParametersType() and ResultType() methods) and then assert on those methods' return values (ParametersType() and ResultType()) rather than the concrete type name (typedActivityFunc). Ensure you remove the require.True check against *typedActivityFunc and replace it with a type assertion or interface check against TypedActivityFunc and then verify reflect.TypeOf(Person{}) and reflect.TypeOf("") via ParametersType() and ResultType(). ``` </details> </blockquote></details> </blockquote></details> <details> <summary>🤖 Prompt for all review comments with AI agents</summary>Verify each finding against the current code and only fix it if needed.
Inline comments:
In@activities/child_workflow_activity.go:
- Around line 17-20: ChildWorkflowInput (and other DTOs like
HTTPActivity.Timeout, ShellActivity.Timeout, RetryConfig, WaitSignalConfig)
currently use time.Duration as a JSON-bound field which causes string forms like
"10m" to unmarshal to 0; change the wire format to a string and parse it
explicitly in the activity: update the JSON-facing structs to accept a string
(or implement a custom UnmarshalJSON for a DurationString type) for the timeout
fields, then call time.ParseDuration on the parsed string inside the
activity/handler and pass the resulting time.Duration into
ChildWorkflowSpec.Timeout (and the equivalent downstream places); ensure errors
from ParseDuration are handled and surfaced rather than silently defaulting to
zero.In
@activities/contrib/shell_activity.go:
- Line 21: The Timeout field on ShellInput is being interpreted as nanoseconds
when numeric JSON/YAML values are provided; implement a fix by adding a custom
duration type (e.g., type Duration) with an UnmarshalJSON method and changing
ShellInput.Timeout to that type so it accepts either a string duration ("5s",
"5000ms") or a bare number interpreted as seconds (or otherwise validate and
emit a clear error); update ShellInput's struct comment to document accepted
formats and add validation in UnmarshalJSON to return a helpful error message
for ambiguous numeric inputs; reference ShellInput, Timeout, and UnmarshalJSON
in activities/contrib/shell_activity.go when making the change.In
@activities/httpx/http_activity.go:
- Line 22: Add a custom UnmarshalJSON on the HTTPInput type to accept numeric
timeouts as seconds and string durations; implement an alias struct with Timeout
as interface{}, json.Unmarshal into it, then if aux.Timeout is a float64 set
h.Timeout = time.Duration(v) * time.Second, if it's a string parse with
time.ParseDuration and return an error wrapped with fmt.Errorf on parse failure;
ensure the method signature is func (h *HTTPInput) UnmarshalJSON(data []byte)
error and import encoding/json, time, and fmt as needed to compile.In
@activity_registry.go:
- Around line 28-39: Register currently panics for a nil receiver or when
r.activities is nil; update Register (receiver *ActivityRegistry) to first guard
against a nil receiver (return a descriptive error) and then ensure r.activities
is initialized (if r.activities == nil { make(map[string]Activity) }) before
writing; keep the existing duplicate-name check using ErrDuplicateActivity and
then store the activity.In
@branch_join_test.go:
- Around line 161-175: The combine activity (registered via
reg2.MustRegister(ActivityFunc("combine", ...))) currently uses require.Equal
inside the activity body which can call t.FailNow() from a goroutine; instead
remove those require.* calls and either (a) perform runtime checks inside
combine and return an error (fmt.Errorf(...)) when expectations fail, or (b)
stop validating inside the activity and move the assertions to the test after
execution.Execute() completes by inspecting the workflow result / captured
branch state. Update the combine activity to only return its computed result or
an error, and add the require.Equal assertions immediately after
execution.Execute() in the test.In
@branch_local_state.go:
- Around line 76-80: The method BranchLocalState.variablesMap() is unused;
either remove it to eliminate dead code or clarify intended future use by adding
a TODO comment and rationale above the function (e.g., "preserved for engine
snapshot/restore — add callers in X module") so reviewers know it's
intentionally kept; search for any planned snapshot/restore code paths before
deleting and update BranchLocalState.variablesMap's comment accordingly.In
@branch.go:
- Around line 1028-1046: resolveEachItems currently treats any string as raw
script via evaluateExpression, breaking literal single-item lists and the new
${...}-only templates; update resolveEachItems to send Each.Items through the
same template/contextual-typing path used by other parameter values instead of
unconditionally calling evaluateExpression. Specifically, remove the
unconditional string->evaluateExpression branch and instead call the
branch-level template evaluation function (the same helper used elsewhere for
parameters—e.g., the resolve/resolveParam/evaluateTemplate helper used by p) to:
- detect and render ${...} templates into an array or single value, 2) coerce a
rendered single value into a []any with one element for literal strings, and 3)
preserve existing []any handling; use the symbols resolveEachItems, Each.Items
and evaluateExpression only as references to locate and replace the logic.In
@checkpoint.go:
- Around line 13-17: The readers currently only reject SchemaVersion >
CheckpointSchemaVersion and thus accept missing/old schemas (SchemaVersion ==
0); update each checkpoint reader (memory_checkpointer.go, execution.go,
checkpointer_file.go) to also reject SchemaVersion < 1 by adding a
minimum-version check alongside the existing CheckpointSchemaVersion comparison
(use the SchemaVersion field and the existing CheckpointSchemaVersion constant),
return an error when SchemaVersion < 1 or SchemaVersion >
CheckpointSchemaVersion, and add a unit test (e.g.,
TestCheckpointOlderSchemaVersionIsRejected) that attempts to load a checkpoint
with SchemaVersion == 0 and expects a failure; alternatively update the
top-of-file docs near the existing comment to explicitly state that consumers
must reject SchemaVersion == 0 if you prefer documentation-only—prefer the code
change to ensure safety.In
@child_workflow.go:
- Around line 190-205: The ExecuteSync wrapper is returning execErr directly
while execution.Execute(execCtx) encodes failures in its ExecutionResult (not
always in execErr), so failed child workflows can be returned as (status=failed,
nil); update the post-execution logic in ExecuteSync to inspect the
ExecutionResult (result) and execution.Status(): if the status indicates failure
or cancelled, return a non-nil error instead of nil (propagate execErr if
present, otherwise construct and return an error that includes
execution.Status() and any useful result information) while still populating and
returning the ChildWorkflowResult struct (ExecutionID, Status, Duration,
Outputs) as before.In
@context.go:
- Around line 24-115: You changed the exported Context interface by
adding/renaming methods (notably ReportProgress), which breaks existing custom
contexts and tests; revert Context to its original shape and move new
functionality into an optional side interface (e.g., declare type
ProgressReporter interface { ReportProgress(detail ProgressDetail) }), update
call sites to type-assert the Context to ProgressReporter before invoking
ReportProgress, and ensure any new helpers/wrappers use that side interface so
existing Context implementers are not forced to change.In
@docs/production_checklist.md:
- Around line 31-33: The doc incorrectly states Runner as the production-only
entry point; update the wording around the Runner and Execution.Execute to
clarify that Runner is a recommended, convenient production entrypoint but
optional — consumers may call Execution.Execute directly and implement their own
heartbeat, timeout, resume, and completion orchestration. Edit the section that
currently claims "Runner (not bareExecution.Execute) is the production entry
point" to explain that Runner composes those concerns for convenience, while
Execution.Executeand the modular heartbeat/resume hooks can be used directly
in custom deployments.In
@examples/join_branches/main.go:
- Around line 123-137: The code calls execution.Execute(ctx) but only checks
err; instead capture the returned ExecutionResult (e.g., result, err :=
execution.Execute(ctx)), then verify the result indicates success (check fields
like result.Error or result.Success / result.Failed() depending on the
ExecutionResult API) before printing "Workflow completed" or reading outputs; if
the result signals failure, log or handle that failure and avoid calling
execution.GetOutputs() and printing outputs until the result is confirmed
successful.In
@examples/retry_simple/main.go:
- Around line 66-67: Assign the result of execution.Execute(...) to a variable
(e.g., res, err := execution.Execute(ctx)), then after the nil-error check also
verify the returned ExecutionResult indicates success before proceeding to
marshal outputs; for example, inspect the ExecutionResult's status/fields
(ExecutionResult, ExecutionResult.Status or any IsSuccessful/Failed helper) and
log/exit if the workflow failed even when err == nil so you don't serialize
outputs from a failed run.In
@execution_callbacks.go:
- Around line 14-16: Restore the original exported path-level methods/types on
ExecutionCallbacks (re-add the existing BeforePathExecution/AfterPathExecution
signatures and any existing PathExecutionEvent type) so existing implementations
keep compiling, then introduce a new optional side interface (e.g.,
BranchExecutionCallbacks with BeforeBranchExecution(ctx, *BranchExecutionEvent)
and AfterBranchExecution(ctx, *BranchExecutionEvent)) or an adapter type for
branch terminology; update CallbackChain to detect and fan out to both the
original path-level callbacks and the new BranchExecutionCallbacks (and map
events between PathExecutionEvent and BranchExecutionEvent as needed) so new
branch hooks are supported without breaking existing implementations.In
@execution_test.go:
- Around line 1151-1192: The activities modify_state_alpha, modify_state_beta,
and modify_state_gamma perform test assertions using require.* inside activity
goroutines, which violates Go's testing rules; change each activity to validate
via plain condition checks and return an error (e.g., return nil,
errors.New("...") ) when validation fails instead of calling require.*, then
move the require.NoError / require.Equal assertions into the test body after
calling execution.Execute to assert overall execution status and errors from
those activities.In
@pause.go:
- Around line 200-235: mutatePauseInCheckpoint must guard against a nil
Checkpointer to avoid panics: at the start of mutatePauseInCheckpoint check if
cp == nil and return a descriptive error (e.g., fmt.Errorf("nil Checkpointer"))
instead of proceeding to the type assertion or calling
AtomicUpdate/LoadCheckpoint; this ensures PauseBranchInCheckpoint and
UnpauseBranchInCheckpoint return an error rather than panicking when cp is nil.In
@script/eval.go:
- Around line 39-46: The current NewTemplate precheck using strings.Count can
miss cases like '"} ${foo"' and silently accept malformed templates; modify
NewTemplate to validate that every "${" has a matching closing "}" that comes
after it by using templateExprRE: run
templateExprRE.FindAllStringSubmatchIndex(raw, -1) and if there are zero matches
but raw contains "${" return an error, and more generally ensure for each match
that the submatch indices correspond to a "${...}" occurrence; if any "${" lacks
a corresponding regex match (i.e. a "${" index that isn't covered by a match's
start index) return a descriptive error instead of falling back to
&Template{raw: raw}; keep references to NewTemplate and templateExprRE to locate
changes.
Outside diff comments:
In@child_workflow.go:
- Around line 285-305: The goroutine that calls execution.Execute(execCtx)
currently discards its returned ExecutionResult/error; capture that terminal
result and store it together with the handle in the asyncExecutions map so
GetResult() can return the real outcome and timing. Specifically, change the
asyncExecutions value to include a field for the terminal result/error (or add a
separate results map keyed by execution.ID()), assign the result of
execution.Execute(execCtx) into that storage before deleting the entry, and
ensure the existing cleanup logic (the time.Sleep cleanup goroutine) does not
delete the stored result before GetResult() reads it; update GetResult() to
consult the stored ExecutionResult/error for the given execution.ID() instead of
synthesizing a generic message. Ensure uses of asyncExecutionsMtx protect both
writing the result and deleting the entry.In
@cmd/workflow/main.go:
- Around line 134-139: The code currently discards the ExecutionResult returned
by execution.Execute and only checks err, which can be nil while result.Status
indicates failure; capture the returned ExecutionResult (e.g., result := ...)
from Execute and pass that result into showExecutionResults instead of only
passing execution and err so showExecutionResults can inspect result.Status;
update the call sites and any function signature of showExecutionResults if
needed to accept the ExecutionResult (or add an overload) and use result for
final success/failure determination.- Around line 279-289: The CLI help advertises a "wait" activity but
createActivityRegistry no longer registers it; fix by restoring the built-in or
removing it from the usage text: either add the Wait activity back into
createActivityRegistry (e.g., insert the wait activity constructor into the
activityList alongside activities.NewPrintActivity(),
activities.NewTimeActivity(), etc.) or remove "wait" from the CLI usage/help
string where activities are listed (search for the help/usage text that mentions
"wait"); ensure the symbol names createActivityRegistry and the wait activity
constructor you add/remove are updated consistently so help and the registered
activities no longer drift.In
@context.go:
- Around line 264-306: The helpers WithTimeout and WithCancel currently replace
non-*executionContext parents with a fresh executionContext losing the parent's
state; instead, after creating ctx and cancel (ctx, cancel :=
context.WithTimeout(parent, timeout) / context.WithCancel(parent)), return the
original parent along with cancel when parent is not a *executionContext so
arbitrary Context implementations are preserved (i.e., in WithTimeout and
WithCancel, change the final return from &executionContext{Context: ctx} to
return parent, cancel).In
@examples/branching/main.go:
- Around line 243-248: Update the example summary strings printed in main (the
fmt.Println calls) so item 4 no longer says "Script activities for calculations"
and instead reflects that the example uses Go activities (e.g., change to "Go
activities for calculations" or similar); locate the fmt.Println lines in main()
that print the numbered list (the five fmt.Println calls shown) and modify the
fourth entry text accordingly to remove the stale "Script activities" wording.In
@examples/error_handling/main.go:
- Around line 118-126: The current code treats a nil error from
execution.Execute(...) as success, but Execute can return nil err while the
workflow finished with a failed status; check execution.Status() after Execute
returns and treat non-success (e.g., "Failed" or not "Completed") as an error
case. Update the post-Execute logic around execution.Status() (and optionally
execution.GetOutputs()) to print a failure message and call os.Exit(1) when the
status indicates failure, otherwise print the success messages as currently
done.In
@execution_callbacks_test.go:
- Around line 209-217: The test currently asserts eventTypes contains only the
six non-failure hooks (using require.Equal on eventTypes) but omits the failure
hooks; update the assertion that checks eventTypes (the map used in the
require.Equal) to include "OnWorkflowExecutionFailure", "OnBranchFailure", and
"OnActivityFailure" set to true (or replace the single require.Equal with
explicit require.True checks for each of those keys) so the test explicitly
verifies those failure callbacks are present when callbacks fire (targets: the
eventTypes variable and the existing require.Equal/assertion around it).In
@README.md:
- Around line 55-73: The example assigns the activity result to branch state via
Step.Store but never declares a workflow output, so calls like
result.OutputString("result") read nothing; update the workflow definition
returned by workflow.New (workflow.Options) to include an Outputs entry
declaring "result" and map it to the stored state key (e.g., state.result or the
framework's output path) so that ExecutionResult.OutputString("result") can read
the value; adjust the same pattern used in the other occurrence (lines ~98-100)
to declare the output there as well.
Nitpick comments:
In@activity_functions_test.go:
- Around line 79-83: The test is asserting the concrete unexported backing type
(*typedActivityFunc[Person, string]) which couples the test to an implementation
detail; instead, change the assertion to check that adapter.Activity() satisfies
the exported TypedActivityFunc[Person, string] interface (or at minimum exposes
the ParametersType() and ResultType() methods) and then assert on those methods'
return values (ParametersType() and ResultType()) rather than the concrete type
name (typedActivityFunc). Ensure you remove the require.True check against
*typedActivityFunc and replace it with a type assertion or interface check
against TypedActivityFunc and then verify reflect.TypeOf(Person{}) and
reflect.TypeOf("") via ParametersType() and ResultType().In
@branch_join_test.go:
- Around line 66-78: The activities "double", "triple", and "sum" use unchecked
type assertions (e.g., value.(int), doubled.(int), tripled.(int)) which can
panic; update each ActivityFunc handler to safely assert types by capturing the
comma-ok result (and check ctx.Get errors) and return a descriptive error if the
value is missing or not an int instead of asserting blindly; specifically change
the "double" and "triple" handlers to verify that ctx.Get("value") yields an int
before using it, and change the "sum" handler to verify that ctx.Get("doubled")
and ctx.Get("tripled") yield ints before adding, returning an error when checks
fail.In
@checkpoint_test.go:
- Around line 81-84: The compile-time interface assertions currently inside
TestAtomicCheckpointerInterface should be moved to package-level declarations:
replace the two lines in TestAtomicCheckpointerInterface (the var _ checks
referencing workflow.Checkpointer, workflow.AtomicCheckpointer and
workflowtest.MemoryCheckpointer) with package-level var _ = ... style assertions
placed outside any function (e.g., at top of the file), and remove or simplify
TestAtomicCheckpointerInterface accordingly so the assertions are evaluated at
compile time without being inside a test function.In
@docs/suspension.md:
- Around line 89-91: Replace the explicit internal field reference
"BranchState.ActivityHistory" in the docs with a description that it is a
per-step replay cache (or "per-step history cache") and avoid naming any backing
field; update the sentence that currently reads "TheHistorycache is
per-step: it lives onBranchState.ActivityHistoryand is cleared when the step
advances past the activity." to instead say something like "The history is a
per-step replay cache that is cleared when the step advances past the activity"
so the docs describe behavior without exposing internal storage names.In
@planning/review/combined_api_review.md:
- Around line 403-409: The fenced code block showing the activity directory
listing (lines containing "activities/", "activities/httpx/",
"activities/contrib/") is missing a language specifier; update the opening fence
to include a language (for example changetotext or ```bash) so the
block is rendered with the correct syntax highlighting and preserved formatting.In
@planning/review/v1_implementation_plan.md:
- Around line 300-305: The fenced code block containing the directory list (the
block starting with triple backticks above the lines "activities/ #
safe, in-process primitives only" through "activities/contrib/ # shell,
file, anything environment-specific or risky") needs a language specifier to
ensure proper Markdown rendering; update the opening fence fromtotext
(or another appropriate language token) so the block is marked as plain text in
the file.In
@typed_activity_example_test.go:
- Line 44: Rename the BranchID test value to use the new terminology: change the
literal assigned to the BranchID field (in the typed_activity_example_test.go
example/test) from "path1" to "branch1" so the BranchID value matches v1 naming
conventions.</details> <details> <summary>🪄 Autofix (Beta)</summary> Fix all unresolved CodeRabbit comments on this PR: - [ ] <!-- {"checkboxId": "4b0d0e0a-96d7-4f10-b296-3a18ea78f0b9"} --> Push a commit to this branch (recommended) - [ ] <!-- {"checkboxId": "ff5b1114-7d8c-49e6-8ac1-43f82af23a33"} --> Create a new PR with the fixes </details> --- <details> <summary>ℹ️ Review info</summary> <details> <summary>⚙️ Run configuration</summary> **Configuration used**: defaults **Review profile**: CHILL **Plan**: Pro **Run ID**: `da94b2be-b09a-4148-a507-7f0cb823eb6d` </details> <details> <summary>📥 Commits</summary> Reviewing files that changed from the base of the PR and between d6ad3373590fd97f1046ede52a1c4cc00839fcbd and a46278d0b31e5f92524d5f034f083ee18844348b. </details> <details> <summary>📒 Files selected for processing (114)</summary> * `CLAUDE.md` * `MIGRATION.md` * `README.md` * `activities/child_workflow_activity.go` * `activities/child_workflow_activity_test.go` * `activities/contrib/file_activity.go` * `activities/contrib/file_activity_test.go` * `activities/contrib/helpers_test.go` * `activities/contrib/shell_activity.go` * `activities/contrib/shell_activity_test.go` * `activities/helpers_test.go` * `activities/httpx/helpers_test.go` * `activities/httpx/http_activity.go` * `activities/httpx/http_activity_test.go` * `activities/print_activity.go` * `activities/wait_activity.go` * `activities/wait_activity_test.go` * `activity.go` * `activity_functions.go` * `activity_functions_test.go` * `activity_history.go` * `activity_history_test.go` * `activity_logger.go` * `activity_registry.go` * `branch.go` * `branch_join_test.go` * `branch_local_state.go` * `branch_state.go` * `branch_test.go` * `checkpoint.go` * `checkpoint_test.go` * `checkpointer.go` * `checkpointer_file.go` * `child_workflow.go` * `cmd/workflow/main.go` * `context.go` * `coverage_test.go` * `docs/production_checklist.md` * `docs/suspension.md` * `errors.go` * `errors_test.go` * `examples/branching/main.go` * `examples/callbacks/main.go` * `examples/child_workflows/README.md` * `examples/child_workflows/main.go` * `examples/durable_sleep/main.go` * `examples/edge_matching/main.go` * `examples/error_handling/main.go` * `examples/expr/README.md` * `examples/expr/basic/main.go` * `examples/expr/compile_once/main.go` * `examples/expr/funcs/main.go` * `examples/expr/higher_order/main.go` * `examples/expr/structs/main.go` * `examples/expr/workflow/main.go` * `examples/fenced_checkpointer/main.go` * `examples/join_branches/README.md` * `examples/join_branches/main.go` * `examples/join_paths/main.go` * `examples/pause_unpause/main.go` * `examples/retry/main.go` * `examples/retry_simple/main.go` * `examples/runner/main.go` * `examples/signal_wait/main.go` * `examples/simple/main.go` * `examples/step_progress/main.go` * `examples/structured_result/main.go` * `execution.go` * `execution_adapter.go` * `execution_callbacks.go` * `execution_callbacks_test.go` * `execution_result.go` * `execution_result_test.go` * `execution_state.go` * `execution_test.go` * `llms.txt` * `path_join_test.go` * `path_local_state.go` * `pause.go` * `pause_test.go` * `planning/review/combined_api_review.md` * `planning/review/v1_implementation_plan.md` * `production_readiness_test.go` * `progress.go` * `review_fixes_test.go` * `run_or_resume_test.go` * `runner.go` * `runner_suspension_test.go` * `runner_test.go` * `script/eval.go` * `script/eval_test.go` * `script_compiler_test.go` * `signal_store.go` * `sleep_test.go` * `step.go` * `step_progress.go` * `step_progress_test.go` * `typed_activity_example_test.go` * `validate.go` * `validate_test.go` * `variable_container.go` * `variable_container_test.go` * `wait.go` * `wait_state.go` * `wait_test.go` * `workflow.go` * `workflow_formatter.go` * `workflow_test.go` * `workflowtest/fake_context.go` * `workflowtest/fake_context_test.go` * `workflowtest/memory_checkpointer.go` * `workflowtest/mock.go` * `workflowtest/workflowtest.go` * `workflowtest/workflowtest_test.go` </details> <details> <summary>💤 Files with no reviewable changes (13)</summary> * activity.go * examples/expr/README.md * activities/wait_activity_test.go * examples/expr/compile_once/main.go * examples/expr/basic/main.go * examples/expr/higher_order/main.go * examples/join_paths/main.go * examples/expr/funcs/main.go * path_join_test.go * examples/expr/workflow/main.go * examples/expr/structs/main.go * activities/wait_activity.go * path_local_state.go </details> <details> <summary>👮 Files not reviewed due to content moderation or server errors (19)</summary> * activities/helpers_test.go * progress.go * step_progress.go * activity_history.go * execution.go * examples/signal_wait/main.go * llms.txt * execution_state.go * examples/structured_result/main.go * activities/child_workflow_activity_test.go * examples/runner/main.go * step_progress_test.go * examples/callbacks/main.go * execution_adapter.go * examples/step_progress/main.go * examples/pause_unpause/main.go * branch_test.go * MIGRATION.md * errors.go </details> </details> <!-- This is an auto-generated comment by CodeRabbit for review status -->
| type ChildWorkflowInput struct { | ||
| WorkflowName string `json:"workflow_name"` | ||
| Sync bool `json:"sync"` | ||
| Inputs map[string]interface{} `json:"inputs"` | ||
| Timeout float64 `json:"timeout"` | ||
| Timeout time.Duration `json:"timeout"` |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's look at the actual file content around the specified lines
cat -n activities/child_workflow_activity.go | head -60Repository: deepnoodle-ai/workflow
Length of output: 2372
🏁 Script executed:
# Check if there are any custom UnmarshalJSON methods for ChildWorkflowInput
rg "UnmarshalJSON|MarshalJSON" activities/child_workflow_activity.go -A 10Repository: deepnoodle-ai/workflow
Length of output: 48
🏁 Script executed:
# Search for how ChildWorkflowInput is used - especially JSON binding
rg "ChildWorkflowInput" activities/ -B 2 -A 5Repository: deepnoodle-ai/workflow
Length of output: 1895
🏁 Script executed:
# Look for time.Duration JSON handling patterns in the codebase
rg "time\.Duration" activities/ -B 2 -A 2Repository: deepnoodle-ai/workflow
Length of output: 1366
🏁 Script executed:
# Search for tests related to ChildWorkflowActivity
find . -name "*test.go" -o -name "*_test.go" | xargs rg "ChildWorkflowActivity|ChildWorkflowInput" 2>/dev/nullRepository: deepnoodle-ai/workflow
Length of output: 439
🏁 Script executed:
# Look for how activity parameters are bound from JSON/workflow parameters
rg "Execute.*params|UnmarshalJSON|json.Unmarshal" activities/ -B 3 -A 5 | head -100Repository: deepnoodle-ai/workflow
Length of output: 5207
🏁 Script executed:
# Check if there's documentation on timeout format expectations
rg -i "timeout.*format|timeout.*duration|30s|1h|nanosecond" . -B 2 -A 2 | head -80Repository: deepnoodle-ai/workflow
Length of output: 4104
🏁 Script executed:
# Look at the Activity interface to understand parameter binding
rg "type Activity|type TypedActivity|func.*Activity.*Execute" --glob "*.go" -B 2 -A 5 | head -100Repository: deepnoodle-ai/workflow
Length of output: 5210
🏁 Script executed:
# Let's examine the ChildWorkflowActivity test to see how timeout is handled
cat -n activities/child_workflow_activity_test.goRepository: deepnoodle-ai/workflow
Length of output: 2812
🏁 Script executed:
# Look at the Activity interface definition and how parameters are bound
rg "type Activity interface|type TypedActivity|func NewTypedActivity" --glob="*.go" -B 2 -A 15Repository: deepnoodle-ai/workflow
Length of output: 2145
🏁 Script executed:
# Check if there's any custom timeout parsing or JSON handling for activities
rg "time\.Duration.*json|json.*time\.Duration|UnmarshalJSON.*Duration" -B 3 -A 5Repository: deepnoodle-ai/workflow
Length of output: 7363
🏁 Script executed:
# Check how workflow parameters are actually provided - look at workflow definition/execution code
rg "workflow parameters|step parameters|activity input|params map" --glob="*.go" -B 2 -A 3 | head -80Repository: deepnoodle-ai/workflow
Length of output: 3811
🏁 Script executed:
# Look for any YAML/configuration examples that show timeout format
find . -name "*.yaml" -o -name "*.yml" -o -name "*.json" | xargs rg "timeout" 2>/dev/null | head -20Repository: deepnoodle-ai/workflow
Length of output: 48
🏁 Script executed:
# Test Go's native time.Duration JSON behavior with a simple test
python3 << 'EOF'
import json
import subprocess
# Create a minimal Go test program to verify time.Duration JSON behavior
test_code = '''package main
import (
"encoding/json"
"fmt"
"time"
)
type TestStruct struct {
Timeout time.Duration `json:"timeout"`
}
func main() {
// Test 1: numeric input (nanoseconds)
numericJSON := `{"timeout": 30000000000}` // 30 seconds in nanoseconds
var t1 TestStruct
err := json.Unmarshal([]byte(numericJSON), &t1)
fmt.Printf("Numeric (30s in ns): err=%v, value=%v\\n", err, t1.Timeout)
// Test 2: string input ("30s" format)
stringJSON := `{"timeout": "30s"}`
var t2 TestStruct
err = json.Unmarshal([]byte(stringJSON), &t2)
fmt.Printf("String (\\\"30s\\\"): err=%v, value=%v\\n", err, t2.Timeout)
// Test 3: invalid format
invalidJSON := `{"timeout": "invalid"}`
var t3 TestStruct
err = json.Unmarshal([]byte(invalidJSON), &t3)
fmt.Printf("Invalid: err=%v\\n", err != nil)
}
'''
with open('/tmp/test_duration.go', 'w') as f:
f.write(test_code)
# Run it
result = subprocess.run(['go', 'run', '/tmp/test_duration.go'],
capture_output=True, text=True, timeout=10)
print(result.stdout)
if result.stderr:
print("STDERR:", result.stderr)
EOFRepository: deepnoodle-ai/workflow
Length of output: 864
🏁 Script executed:
# Look at how parameters are documented in examples or README
find . -name "README*" -o -name "*.md" | xargs rg -i "activity.*param|timeout.*format" 2>/dev/null | head -20Repository: deepnoodle-ai/workflow
Length of output: 2119
🏁 Script executed:
# Look at the activity.go implementation more carefully to understand the marshaling flow
sed -n '1,150p' activity.go | tail -50Repository: deepnoodle-ai/workflow
Length of output: 1832
🏁 Script executed:
# Check if there are any documented examples of how to pass timeout to activities
rg "timeout" documentation/ examples/ --glob="*.md" -B 2 -A 2Repository: deepnoodle-ai/workflow
Length of output: 5780
🏁 Script executed:
# Verify Go's time.Duration JSON support by checking if there are any comments or documentation about it
rg "time.Duration" -B 5 -A 5 | grep -i "json\|marshal\|format\|unmarshal" | head -30Repository: deepnoodle-ai/workflow
Length of output: 2564
🏁 Script executed:
# Check step.go to see how WaitSignal.Timeout and other Duration fields are used
sed -n '1,200p' step.go | grep -A 10 -B 5 "Timeout.*Duration"Repository: deepnoodle-ai/workflow
Length of output: 1242
🏁 Script executed:
# Quick verification of Go's time.Duration JSON behavior
cat > /tmp/test_duration.go << 'EOF'
package main
import (
"encoding/json"
"fmt"
"time"
)
type TestInput struct {
Timeout time.Duration `json:"timeout"`
}
func main() {
// Test string format
stringInput := `{"timeout":"30s"}`
var t1 TestInput
err1 := json.Unmarshal([]byte(stringInput), &t1)
fmt.Printf("String format (\"30s\"): success=%v, value=%v\n", err1 == nil, t1.Timeout)
// Test numeric format (nanoseconds)
numInput := `{"timeout":30000000000}`
var t2 TestInput
err2 := json.Unmarshal([]byte(numInput), &t2)
fmt.Printf("Numeric format (30s in ns): success=%v, value=%v\n", err2 == nil, t2.Timeout)
}
EOF
cd /tmp && go run test_duration.goRepository: deepnoodle-ai/workflow
Length of output: 164
🏁 Script executed:
# Check the Go version used in the project to confirm time.Duration JSON support
head -20 go.modRepository: deepnoodle-ai/workflow
Length of output: 163
🏁 Script executed:
# Verify the actual behavior in context by checking if there are any workarounds or custom unmarshalers
rg "UnmarshalJSON|Duration.*json|json.*Duration" --glob="*.go" -B 5 -A 10 | head -100Repository: deepnoodle-ai/workflow
Length of output: 4936
🏁 Script executed:
# Check if the documentation examples actually match what the code accepts
rg "timeout.*[0-9]" documentation/child-workflows.md examples/ --glob="*.md" --glob="*.go"Repository: deepnoodle-ai/workflow
Length of output: 608
time.Duration as a JSON-bound activity input field is a silent data loss risk.
The wire format only accepts nanosecond integers—string forms like "30s" silently fail to unmarshal and become 0. This directly contradicts the documentation (e.g., child-workflows.md shows "timeout": "10m"), creating a compatibility trap where workflow parameters will silently lose their timeout values. The same issue affects HTTPActivity.Timeout, ShellActivity.Timeout, and duration fields in RetryConfig and WaitSignalConfig.
Use a string wire format and parse it in the activity before passing to ChildWorkflowSpec.Timeout to avoid silent data corruption.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@activities/child_workflow_activity.go` around lines 17 - 20,
ChildWorkflowInput (and other DTOs like HTTPActivity.Timeout,
ShellActivity.Timeout, RetryConfig, WaitSignalConfig) currently use
time.Duration as a JSON-bound field which causes string forms like "10m" to
unmarshal to 0; change the wire format to a string and parse it explicitly in
the activity: update the JSON-facing structs to accept a string (or implement a
custom UnmarshalJSON for a DurationString type) for the timeout fields, then
call time.ParseDuration on the parsed string inside the activity/handler and
pass the resulting time.Duration into ChildWorkflowSpec.Timeout (and the
equivalent downstream places); ensure errors from ParseDuration are handled and
surfaced rather than silently defaulting to zero.
| WorkingDir string `json:"working_dir"` | ||
| Environment map[string]string `json:"environment"` | ||
| Timeout float64 `json:"timeout"` // in seconds, 0 means no timeout | ||
| Timeout time.Duration `json:"timeout"` // 0 means no timeout |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# 1) Find how typed activity params are decoded into structs
rg -n -C3 'func NewTypedActivity|TypedActivityFunc|json\.Marshal|json\.Unmarshal|mapstructure|Decode' --type go
# 2) Find non-test timeout examples/usages to confirm expected units
rg -n -C2 '"timeout"\s*:' --type go --type md --type yaml --type yml --type json -g '!**/*_test.go'
# 3) Locate docs that define timeout units for shell/contrib activities
rg -n -C2 'shell|timeout|duration|seconds|time\.Duration' --type mdRepository: deepnoodle-ai/workflow
Length of output: 21601
🏁 Script executed:
# Find and inspect shell_activity.go to see actual timeout implementation
fd -type f -name "shell_activity.go" -exec cat -n {} +Repository: deepnoodle-ai/workflow
Length of output: 236
🏁 Script executed:
# Search for shell activity usage examples and tests
rg -n "shell_activity|ShellInput|ShellActivity" --type go -A 5 -B 2 | head -100Repository: deepnoodle-ai/workflow
Length of output: 3085
🏁 Script executed:
# Look for any timeout-related documentation or examples
fd -type f \( -name "*.md" -o -name "*.txt" -o -name "README*" \) -path "*/activities/*" | xargs rg -l "timeout" 2>/dev/null || trueRepository: deepnoodle-ai/workflow
Length of output: 1353
🏁 Script executed:
# Check how shell activity timeout is actually used in examples/tests
rg -n "Timeout.*time\.Duration|timeout.*:" activities/contrib/ --type go -A 3 -B 3Repository: deepnoodle-ai/workflow
Length of output: 1779
🏁 Script executed:
# Read complete shell_activity.go to see all timeout handling
cat -n activities/contrib/shell_activity.goRepository: deepnoodle-ai/workflow
Length of output: 2793
🏁 Script executed:
# Search for shell activity usage in workflow definitions or examples
rg -n "shell" --type-list | head -10 && rg -n '"shell"' -A 10 -B 2 | head -80Repository: deepnoodle-ai/workflow
Length of output: 2310
🏁 Script executed:
# Look for any example workflows that use the shell activity
find . -name "*.json" -o -name "*.yaml" -o -name "*.yml" | xargs rg -l "shell" 2>/dev/null | head -10Repository: deepnoodle-ai/workflow
Length of output: 48
🏁 Script executed:
# Check TypedActivity parameter decoding to understand time.Duration JSON unmarshal behavior
rg -n "time\.Duration|UnmarshalJSON" activities/contrib/ -A 3Repository: deepnoodle-ai/workflow
Length of output: 353
🏁 Script executed:
# Search for examples of shell activity being used in workflows
rg -n "shell|timeout" examples/ -A 5 -B 2 | head -100Repository: deepnoodle-ai/workflow
Length of output: 6505
🏁 Script executed:
# Check README and docs for shell activity documentation
cat -n activities/contrib/README.md 2>/dev/null || echo "No README found"Repository: deepnoodle-ai/workflow
Length of output: 81
🏁 Script executed:
# Look for migration guide or breaking changes related to timeout
rg -n "timeout|duration|shell" MIGRATION.md -A 3 -B 3 | head -80Repository: deepnoodle-ai/workflow
Length of output: 1255
🏁 Script executed:
# Check if there are any workflow test files that define timeouts
find . -name "*test*.json" -o -name "*example*.json" | xargs rg -l "timeout" 2>/dev/null | head -10Repository: deepnoodle-ai/workflow
Length of output: 231
Timeout unit regression: numeric JSON values become nanoseconds instead of seconds.
When workflow definitions provide numeric timeout values (e.g., "timeout": 5), Go's standard time.Duration JSON unmarshal interprets them as nanoseconds. This causes timeout: 5 to become 5 nanoseconds, triggering near-immediate cancellation instead of the likely-intended 5 seconds.
The migration to time.Duration is documented in MIGRATION.md (lines 213–215), but the expected format for timeout in workflow definitions is unclear. Examples show Go code using 30 * time.Second, not JSON/YAML input. Consider:
- Add documentation to
ShellInput(line 21) clarifying that timeout must be a string duration format ("5s","5000ms") or a numeric nanosecond value if provided via workflow definitions. - Add validation or a helpful error message if a user provides an ambiguous numeric format.
- Update examples to demonstrate correct timeout format in workflow definitions.
Also applies to: 43-43
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@activities/contrib/shell_activity.go` at line 21, The Timeout field on
ShellInput is being interpreted as nanoseconds when numeric JSON/YAML values are
provided; implement a fix by adding a custom duration type (e.g., type Duration)
with an UnmarshalJSON method and changing ShellInput.Timeout to that type so it
accepts either a string duration ("5s", "5000ms") or a bare number interpreted
as seconds (or otherwise validate and emit a clear error); update ShellInput's
struct comment to document accepted formats and add validation in UnmarshalJSON
to return a helpful error message for ambiguous numeric inputs; reference
ShellInput, Timeout, and UnmarshalJSON in activities/contrib/shell_activity.go
when making the change.
| Body string `json:"body"` // JSON string or plain text | ||
| JSONPayload map[string]any `json:"json_payload"` // Alternative to body for JSON | ||
| Timeout float64 `json:"timeout"` // in seconds, default 30 | ||
| Timeout time.Duration `json:"timeout"` // 0 uses default of 30s |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
How does Go encoding/json handle time.Duration marshaling and unmarshaling?
💡 Result:
In standard encoding/json (as of Go 1.26), time.Duration does not implement json.Marshaler or json.Unmarshaler interfaces, nor encoding.TextMarshaler. Therefore, it is treated as an integer type and marshaled to a JSON number representing its nanosecond value (e.g., time.Second marshals to 1000000000). Unmarshaling from a JSON number sets the Duration to that integer value interpreted as nanoseconds; unmarshaling from a string or other types fails with a type error. Developers commonly wrap time.Duration in a custom type implementing MarshalJSON/UnmarshalJSON to use the human-readable time.Duration.String format (e.g., "1s") for both marshaling (as JSON string) and unmarshaling (parsing via time.ParseDuration, often also accepting numbers). An experimental encoding/json/v2 (enabled via GOEXPERIMENT=jsonv2, proposed in 2025 issues #71497/#71631, available in tip) adds native support for time.Duration with configurable formats via struct tags like json:",format:sec", defaulting to time.Duration.String representation. As of 2026-04-12, this remains experimental and is not the default behavior.
Citations:
- 1: https://stackoverflow.com/questions/48050945/how-to-unmarshal-json-into-durations
- 2: https://go.dev/issue/71631
- 3: https://pkg.go.dev/encoding/json
- 4: https://pkg.go.dev/encoding/json/v2
- 5: https://pkg.go.dev/encoding/json@go1.25.6
- 6: https://go.dev/issue/10275
- 7: https://go.dev/src/encoding/json/encode.go
- 8: proposal: time: add
MarshalJSONandUnmarshalJSONsupport golang/go#65501
🏁 Script executed:
#!/bin/bash
# Locate and examine the HTTPInput struct in http_activity.go
fd -e go -p "http_activity" | head -5Repository: deepnoodle-ai/workflow
Length of output: 138
🏁 Script executed:
#!/bin/bash
# Check the HTTPInput struct definition and surrounding code
cat -n activities/httpx/http_activity.go | head -50Repository: deepnoodle-ai/workflow
Length of output: 2030
🏁 Script executed:
#!/bin/bash
# Look for JSON marshaling methods on HTTPInput or related types
rg -A 5 "(UnmarshalJSON|MarshalJSON)" activities/httpx/ --type goRepository: deepnoodle-ai/workflow
Length of output: 48
🏁 Script executed:
#!/bin/bash
# Check test file for JSON usage patterns
cat -n activities/httpx/http_activity_test.go | head -100Repository: deepnoodle-ai/workflow
Length of output: 3883
🏁 Script executed:
#!/bin/bash
# Search for timeout usage in tests or examples
rg -i "timeout" activities/httpx/ --type go -B 2 -A 2Repository: deepnoodle-ai/workflow
Length of output: 1220
🏁 Script executed:
#!/bin/bash
# Check if there's any conversion or validation logic for Timeout in Execute method
cat -n activities/httpx/http_activity.go | sed -n '48,120p'Repository: deepnoodle-ai/workflow
Length of output: 2491
Add custom UnmarshalJSON to HTTPInput to handle numeric timeout values.
Changing Timeout from float64 to time.Duration introduces a breaking change in JSON deserialization. The standard encoding/json marshals time.Duration as an integer representing nanoseconds. This means:
- Existing workflows with
"timeout": 30(expecting 30 seconds) would be deserialized as 30 nanoseconds - Users must now pass nanosecond values like
"timeout": 30000000000for 30 seconds
Implement a custom UnmarshalJSON method on HTTPInput that interprets numeric JSON values as seconds (for backwards compatibility) while accepting duration strings like "30s":
func (h *HTTPInput) UnmarshalJSON(data []byte) error {
type Alias HTTPInput
aux := &struct {
Timeout interface{} `json:"timeout"`
*Alias
}{
Alias: (*Alias)(h),
}
if err := json.Unmarshal(data, &aux); err != nil {
return err
}
if aux.Timeout != nil {
switch v := aux.Timeout.(type) {
case float64:
h.Timeout = time.Duration(v) * time.Second
case string:
d, err := time.ParseDuration(v)
if err != nil {
return fmt.Errorf("invalid timeout: %w", err)
}
h.Timeout = d
}
}
return nil
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@activities/httpx/http_activity.go` at line 22, Add a custom UnmarshalJSON on
the HTTPInput type to accept numeric timeouts as seconds and string durations;
implement an alias struct with Timeout as interface{}, json.Unmarshal into it,
then if aux.Timeout is a float64 set h.Timeout = time.Duration(v) * time.Second,
if it's a string parse with time.ParseDuration and return an error wrapped with
fmt.Errorf on parse failure; ensure the method signature is func (h *HTTPInput)
UnmarshalJSON(data []byte) error and import encoding/json, time, and fmt as
needed to compile.
| // Path-level callbacks | ||
| BeforePathExecution(ctx context.Context, event *PathExecutionEvent) | ||
| AfterPathExecution(ctx context.Context, event *PathExecutionEvent) | ||
| BeforeBranchExecution(ctx context.Context, event *BranchExecutionEvent) | ||
| AfterBranchExecution(ctx context.Context, event *BranchExecutionEvent) |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Keep ExecutionCallbacks backward-compatible.
Renaming the exported path hooks/events forces every custom callback implementation to stop compiling. Please preserve the existing methods/types and add branch terminology through a side interface or adapter, then have CallbackChain fan out to both.
Based on learnings: Never modify exported interfaces; use optional side interfaces (such as the ProgressReporter pattern) to add new functionality.
Also applies to: 37-42, 55-56
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@execution_callbacks.go` around lines 14 - 16, Restore the original exported
path-level methods/types on ExecutionCallbacks (re-add the existing
BeforePathExecution/AfterPathExecution signatures and any existing
PathExecutionEvent type) so existing implementations keep compiling, then
introduce a new optional side interface (e.g., BranchExecutionCallbacks with
BeforeBranchExecution(ctx, *BranchExecutionEvent) and AfterBranchExecution(ctx,
*BranchExecutionEvent)) or an adapter type for branch terminology; update
CallbackChain to detect and fan out to both the original path-level callbacks
and the new BranchExecutionCallbacks (and map events between PathExecutionEvent
and BranchExecutionEvent as needed) so new branch hooks are supported without
breaking existing implementations.
| reg5.MustRegister(ActivityFunc("modify_state_alpha", func(ctx Context, params map[string]any) (any, error) { | ||
| // Verify we start with the setup value | ||
| counter, ok := ctx.Get("shared_counter") | ||
| require.True(t, ok) | ||
| require.Equal(t, 100, counter) | ||
|
|
||
| // Each branch modifies the same variable name with different values | ||
| ctx.Set("shared_counter", 200) | ||
| ctx.Set("branch_identifier", "ALPHA") | ||
| ctx.Set("multiplier", 2) | ||
|
|
||
| recordBranchExecution("alpha") | ||
| return "alpha-200", nil | ||
| })) | ||
| reg5.MustRegister(ActivityFunc("modify_state_beta", func(ctx Context, params map[string]any) (any, error) { | ||
| // Verify we start with the setup value (not alpha's modification) | ||
| counter, ok := ctx.Get("shared_counter") | ||
| require.True(t, ok) | ||
| require.Equal(t, 100, counter) | ||
|
|
||
| // Each branch modifies the same variable name with different values | ||
| ctx.Set("shared_counter", 300) | ||
| ctx.Set("branch_identifier", "BETA") | ||
| ctx.Set("multiplier", 3) | ||
|
|
||
| recordBranchExecution("beta") | ||
| return "beta-300", nil | ||
| })) | ||
| reg5.MustRegister(ActivityFunc("modify_state_gamma", func(ctx Context, params map[string]any) (any, error) { | ||
| // Verify we start with the setup value (not alpha's or beta's modifications) | ||
| counter, ok := ctx.Get("shared_counter") | ||
| require.True(t, ok) | ||
| require.Equal(t, 100, counter) | ||
|
|
||
| // Each branch modifies the same variable name with different values | ||
| ctx.Set("shared_counter", 400) | ||
| ctx.Set("branch_identifier", "GAMMA") | ||
| ctx.Set("multiplier", 4) | ||
|
|
||
| recordBranchExecution("gamma") | ||
| return "gamma-400", nil | ||
| })) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, understand the internal/require package
find . -name require -type f | head -20
find . -path '*/internal/require*' -type fRepository: deepnoodle-ai/workflow
Length of output: 95
🏁 Script executed:
# Search for internal/require usage and definition
rg -l 'internal/require' --type go | head -20
rg -A 5 'package require' --type goRepository: deepnoodle-ai/workflow
Length of output: 735
🏁 Script executed:
# Check execution_test.go for the full context of these activities
wc -l execution_test.go
sed -n '1140,1200p' execution_test.go
sed -n '1470,1510p' execution_test.goRepository: deepnoodle-ai/workflow
Length of output: 3671
🏁 Script executed:
# Understand how activities are executed - check the ActivityFunc type
ast-grep --pattern 'type ActivityFunc $_'
rg 'func.*ActivityFunc' --type go -B 2 -A 10 | head -50Repository: deepnoodle-ai/workflow
Length of output: 2698
🏁 Script executed:
# Check how Execute runs activities and if they use goroutines
rg 'func.*Execute' --type go | grep -v test | head -20Repository: deepnoodle-ai/workflow
Length of output: 2567
🏁 Script executed:
# Examine the execution_adapter to understand how activities are executed
cat -n execution_adapter.goRepository: deepnoodle-ai/workflow
Length of output: 502
🏁 Script executed:
# Check how ExecuteActivity is called - look for goroutine spawning
rg 'go ' --type go -B 3 -A 3 | grep -A 10 -B 10 'ExecuteActivity\|activity'Repository: deepnoodle-ai/workflow
Length of output: 48
🏁 Script executed:
# Examine the internal/require implementation to understand behavior on assertion failure
cat -n internal/require/require.goRepository: deepnoodle-ai/workflow
Length of output: 13544
🏁 Script executed:
# Search for how activities are invoked during execution
rg 'executeStep\|executeActivity' --type go -A 20 | head -80Repository: deepnoodle-ai/workflow
Length of output: 48
🏁 Script executed:
# Find the executeActivity implementation in execution.go
rg -A 30 'func.*executeActivity' --type go execution.go | head -80Repository: deepnoodle-ai/workflow
Length of output: 1657
🏁 Script executed:
# Search for goroutine spawning related to activity execution
rg 'go.*executeActivity\|go.*Execute' --type go | head -20Repository: deepnoodle-ai/workflow
Length of output: 48
🏁 Script executed:
# Look at execution.go to understand the execution flow
wc -l execution.go
head -200 execution.goRepository: deepnoodle-ai/workflow
Length of output: 7759
🏁 Script executed:
# Check how the orchestrator processes steps and activities
rg 'orchestrator\|worker' --type go -B 2 -A 5 | head -100Repository: deepnoodle-ai/workflow
Length of output: 48
🏁 Script executed:
# Search for how branches are executed - look for goroutine spawning patterns
rg 'runBranch\|processBranch' --type go -A 20 | head -100Repository: deepnoodle-ai/workflow
Length of output: 48
🏁 Script executed:
# Look for the branch execution goroutine pattern
rg 'go func' --type go execution.go | head -20Repository: deepnoodle-ai/workflow
Length of output: 88
🏁 Script executed:
# Search for how the orchestrator runs branches
rg 'type branch struct' --type go -A 30Repository: deepnoodle-ai/workflow
Length of output: 1209
🏁 Script executed:
# Check execution.go for branch execution patterns
sed -n '400,600p' execution.goRepository: deepnoodle-ai/workflow
Length of output: 6807
🏁 Script executed:
# Find the full goroutine pattern that spawns branches
rg 'go func.*branch' --type go -B 5 -A 15Repository: deepnoodle-ai/workflow
Length of output: 877
🏁 Script executed:
# Look for the run() method to understand branch execution
rg 'func.*\(e \*Execution\) run' --type go -A 50Repository: deepnoodle-ai/workflow
Length of output: 6750
🏁 Script executed:
# Check how activities are invoked within the branch execution
rg 'activity.Execute\|executeActivity' --type go -B 5 -A 10 | head -80Repository: deepnoodle-ai/workflow
Length of output: 48
🏁 Script executed:
# Verify the testing.T behavior with FailNow from goroutines
python3 << 'EOF'
# From the internal/require code, when require.True/Equal fails:
# 1. It calls fail(t, msg, msgAndArgs)
# 2. fail() calls t.Errorf() and then t.FailNow()
# 3. FailNow() is *testing.T.FailNow()
# From Go's testing package documentation:
# - FailNow() marks the test as failed and stops execution of the test.
# - Must be called from the test goroutine
# - Calling from non-test goroutines is undefined behavior
print("Go testing.T.FailNow() behavior:")
print("- Designed to be called from the test goroutine only")
print("- Calling from activity goroutines is problematic:")
print(" * May not properly mark test as failed")
print(" * May cause panic or deadlock")
print(" * Test state becomes unpredictable")
print("\nThe review comment's concern is valid:")
print("- Activities execute in branch goroutines")
print("- require.* calls FailNow() in those goroutines")
print("- This violates Go's testing API contract")
EOFRepository: deepnoodle-ai/workflow
Length of output: 484
🏁 Script executed:
# Search for branches being spawned and executed
sed -n '600,900p' execution.goRepository: deepnoodle-ai/workflow
Length of output: 9771
🏁 Script executed:
# Check if there are any other patterns in the test file that show how to properly handle activity assertions
rg 'recordBranchExecution\|return.*error' execution_test.go -B 5 -A 5 | head -60Repository: deepnoodle-ai/workflow
Length of output: 48
🏁 Script executed:
# Look for how the test checks results after execution
sed -n '1200,1250p' execution_test.goRepository: deepnoodle-ai/workflow
Length of output: 1738
🏁 Script executed:
# Look at what recordBranchExecution does and how the test verifies results
sed -n '1050,1150p' execution_test.goRepository: deepnoodle-ai/workflow
Length of output: 3236
🏁 Script executed:
# Check if there are examples of activities returning errors that get checked
rg 'return.*error' execution_test.go | grep -v 'nil' | head -20Repository: deepnoodle-ai/workflow
Length of output: 279
🏁 Script executed:
# Verify the testing.T interface in internal/require to understand the contract
grep -A 5 'type TestingT interface' internal/require/require.goRepository: deepnoodle-ai/workflow
Length of output: 151
Move assertion logic out of activity goroutines.
Activities execute in branch goroutines, not the test goroutine. Calling require.* there invokes FailNow() from a non-test goroutine, violating Go's testing API and potentially leaving the test in an unclear or hanging state. Return an error from the activity when validation fails, and assert on the result in the test body instead.
Example pattern (lines 1151-1192, 1479-1496)
Instead of:
ActivityFunc("modify_state_alpha", func(ctx Context, params map[string]any) (any, error) {
counter, ok := ctx.Get("shared_counter")
require.True(t, ok)
require.Equal(t, 100, counter)
// ...
return "alpha-200", nil
})Return an error and assert in the test body:
ActivityFunc("modify_state_alpha", func(ctx Context, params map[string]any) (any, error) {
counter, ok := ctx.Get("shared_counter")
if !ok || counter != 100 {
return nil, errors.New("shared_counter validation failed")
}
// ...
return "alpha-200", nil
})Then in the test:
result, err := execution.Execute(ctx)
require.NoError(t, err)
require.Equal(t, ExecutionStatusCompleted, result.Status)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@execution_test.go` around lines 1151 - 1192, The activities
modify_state_alpha, modify_state_beta, and modify_state_gamma perform test
assertions using require.* inside activity goroutines, which violates Go's
testing rules; change each activity to validate via plain condition checks and
return an error (e.g., return nil, errors.New("...") ) when validation fails
instead of calling require.*, then move the require.NoError / require.Equal
assertions into the test body after calling execution.Execute to assert overall
execution status and errors from those activities.
- Propagate ExecutionResult in CLI and all examples instead of only
checking error (critical: Execute can return nil err with failed status)
- Remove stale "wait" activity from CLI help text
- Fix stale "Script activities" text in branching example
- Add Outputs declaration to README quick-start example
- Make ActivityRegistry zero value safe (nil receiver + nil map guard)
- Guard against nil Checkpointer in pause/unpause helpers
- Propagate failed child workflow executions as errors from ExecuteSync
- Reject pre-v1 checkpoints (SchemaVersion < 1) in all checkpoint readers
- Reject malformed ${...} templates instead of silently treating as literals
- Clarify Runner is recommended but not required in production checklist
- Remove dead variablesMap() method from BranchLocalState
- Replace require assertions in activity goroutine with error returns
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Propagate ExecutionResult in CLI and all examples instead of only
checking error (critical: Execute can return nil err with failed status)
- Remove stale "wait" activity from CLI help text
- Fix stale "Script activities" text in branching example
- Add Outputs declaration to README quick-start example
- Make ActivityRegistry zero value safe (nil receiver + nil map guard)
- Guard against nil Checkpointer in pause/unpause helpers
- Propagate failed child workflow executions as errors from ExecuteSync
- Reject pre-v1 checkpoints (SchemaVersion < 1) in all checkpoint readers
- Reject malformed ${...} templates instead of silently treating as literals
- Clarify Runner is recommended but not required in production checklist
- Remove dead variablesMap() method from BranchLocalState
- Replace require assertions in activity goroutine with error returns
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Summary
Takes the workflow library from its pre-v1 shape to a stable v1 surface.
Based on the public API review and implementation plan.
What changed (13 PRs)
workflow.Newnow rejects invalid step kinds, bad modifiers, and structural errors upfront instead of at runtime (PR3: validation phase 1 + step kinds + StartAt #22)Execute),NewExecution(wf, reg, ...ExecutionOption), deletedRun/Resume/RunOrResume/ExecuteOrResume(PR4: ActivityRegistry, functional options, single Execute #23)NewExecutionvalidates activity bindings before execution starts (PR5: binding validation in NewExecution #24)Getprefixes, movedWait/History/ReportProgressonto the interface, addedFakeContextfor testing (PR6: Context becomes idiomatic Go #25)Checkpointis opaque; accessor methods replace direct field access (PR7: checkpoint stable wire format #26)${...}only; deleted$(...)(PR8: single template syntax #27)activities/), contrib (activities/contrib/), http (activities/httpx/) (PR9: activities tier split + naming #28)*ExecutionResult(PR12: ExecutionResult helpers #31)Stats
113 files changed, +8853 / −5270 lines
Migration
See MIGRATION.md for every breaking change with before/after snippets.
Test plan
make test-allpasses (tests + vet)🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Breaking Changes
Documentation