Repository navigation
chore(release): promote main to release for @workflowbuilder/temporal 0.1.0 - #166
Merged
Merged
Conversation
* feat(examples): add a StackBlitz-runnable starter for the SDK * fix(examples): point StackBlitz file param at app.tsx * feat(docs): embed the runnable starter and fix its example nodes * fix(examples): show only the preview pane in the StackBlitz embed * fix(docs): fixed link to Stackblitz so it works after the merge
* feat(ui): add @workflowbuilder/ui and @workflowbuilder/ui-tokens packages
* fix(ui): correctness fixes from review
- SegmentPicker: type onChange/onSelect/onClick event as React.MouseEvent
instead of MouseEventHandler (types vs runtime mismatch in public API)
- Modal: apply className and ...rest to the same root element with ref,
not split across the modal and content divs
- Button: log a clear error for children that match no variant instead of
silently rendering null
- tokens: drop the swallowing try/catch in ejectTokens so a failed write
aborts the build instead of feeding empty/stale JSON to the CSS step
- tokens: lift tsconfig lib to es2021 (String.replaceAll)
- add per-package lint-staged configs so in-package tsc runs on commit
* refactor(ui): review polish pass
- remove empty output.json and stale TODO in src/index.ts
- rebrand vite lib + combine-css plugin names away from Overflow UI
- Shape: replace '' sentinel with explicit 'default'
- Snackbar/NodePanel JSDoc match actual behavior; check-built-css comment
no longer references a non-existent stylelint rule
- Switch: drop redundant styles prop (keep className)
- Tooltip: inline the constant open/close delays, drop the speculative
delay context (placement context kept)
- tokens: scope codeChunks to generateCSSBundle; fix double-slash token dir
* test(ui): add Vitest suites for ui + tokens pure logic
Set up Vitest in @workflowbuilder/ui (jsdom) and @workflowbuilder/ui-tokens
(node) and wire both into the root pnpm test.
Extract the pure helpers that were buried in component files into sibling,
non-barrel modules so they can be unit-tested without rendering: date-picker
date-utils (parseDateValue/normalizeInitialValue/dayjsTokenToDateFns), and the
menu/tooltip placement mappers. Public types are re-exported so the package API
is unchanged.
Coverage: button structural guards, rangeBetween, getValidShape, the date
parsing/timezone logic, placement->side/align + offset mapping, and toFileName.
50 ui + 4 tokens tests. Exclude specs from the dts build so they don't ship.
* chore(ui): release-prep for npm publish
- narrow @base-ui/react peer range to ~1.4.0 (1.5/1.6 regressed transitions)
- resolve the bundle-vs-dependency split: date-fns, react-day-picker, clsx and
Phosphor are bundled into dist, so move them to devDependencies; only the
external react-textarea-autosize stays a runtime dependency (peers unchanged)
- rewrite npm description/keywords away from the diagram-library boilerplate
- fix the README dev section (real pnpm --filter commands; no preview-page) and
drop leftover 'Overflow UI' branding
- correct CLAUDE.md: ui-tokens is private (not in changeset ignore)
* ci(release): scoped per-package release tags + ui publish workflow
Retire the single-package v* tag scheme now that the repo publishes two
packages. Each package releases via its own scoped tag and workflow:
- @workflowbuilder/sdk@X.Y.Z -> release-sdk.yml (trigger + version-parse updated)
- @workflowbuilder/ui@X.Y.Z -> release-ui.yml (new; OIDC trusted publisher,
provenance, build:ui, version/idempotency checks, GitHub Release)
Add a UI lint/typecheck/test/build job to pr-check. Update RELEASE.md and
CLAUDE.md for the scoped-tag two-package flow and correct the stale changeset
ignore claim. Each package needs its own npm trusted publisher (manual, noted
in RELEASE.md).
* chore(ui): make @base-ui/react a regular dependency, not a peer
ui is the sole consumer of Base UI and is version-locked to 1.4.x (later
versions regressed transitions), so it should own that pinned version rather
than delegate it to every consumer via a peer range. Move @base-ui/react to
dependencies (catalog: -> 1.4.1 at publish); react / react-dom stay peers as
true singletons. Consistent with react-textarea-autosize, the other external.
* fix(ui): ship react-day-picker styles so the calendar renders
react-day-picker v9 ships no styles of its own; the component never imported
its stylesheet, so the calendar grid, month caption and nav rendered with
browser defaults (vertical nav, bordered day buttons). Import
react-day-picker/style.css so libInjectCss bundles it into dist (and the docs
combined index.css), giving the calendar its real layout.
* fix(ui): theme the date-picker calendar and add day hover
react-day-picker ships a default blue accent and no hover state. Re-theme it
to the design tokens: selected days get a filled --ax selected background
instead of the accent ring, today uses a themed accent color, and day buttons
gain a hover background (react-day-picker has none). New tokens:
--ax-public-date-picker-date-{hover-background,selected,today}-color.
* fix(ui): theme the date-picker month-nav chevrons
react-day-picker paints the prev/next chevrons with --rdp-accent-color, which
defaulted to a raw blue and clashed with the rest of the themed calendar. Paint
them with a neutral nav token instead, and retheme --rdp-accent-color (focus
rings, range endpoints) to the design-token accent so no raw blue remains.
* fix(ui): IconSwitch uncontrolled icon swap + Accordion double toggle
- IconSwitch picked the thumb icon from the controlled `checked` prop, so in
uncontrolled mode the icon never swapped. Render both icons and swap them via
the switch's `data-checked` state in CSS (works controlled + uncontrolled).
- Accordion fired `onToggleOpen` twice when the chevron was clicked: the
Collapsible button's toggle and the header's onClick both ran. The header is
the single click target now (Collapsible is display-only, controlled via
isExpanded); also fixed aria-expanded to reflect isExpanded.
* docs(ui): correct and extend the CHANGELOG for the beta
- @base-ui/react is a regular dependency now, not a peer (the entry still
claimed peer); react / react-dom are the only peers.
- document the review-driven breaking changes (Switch styles prop removed,
Shape '' -> 'default', SegmentPicker event type, Modal prop target) and add a
Fixed section (calendar styling, Accordion double-fire, IconSwitch, Button).
* chore(ui): release as 2.0.0 with a fresh changelog
First release under the @workflowbuilder/ui name, versioned 2.0.0 to sit on the
Workflow Builder 2.x line. Rewrite CHANGELOG.md from scratch: a short note that
the library moved from @synergycodes/overflow-ui (with a link to its old
changelog) plus a single 2.0.0 entry covering the highlights and the migration
deltas. Update the README technology note to match.
* fix(ui): wrap remaining component CSS in cascade layers, guard against regressions
Several component stylesheets (node ports, radio buttons, edge labels,
snackbar, node icon/description, base typography) shipped rule bodies
outside the ui.base/ui.component layers, letting unlayered CSS beat
layered CSS regardless of specificity - the same bug class that shipped
collapsed decision-node ports (WB-190).
Extend check-built-css.ts with a second, independent check that fails
the build if any dist CSS rule sits outside an @layer block, with
documented exceptions for :root custom-property blocks, the deliberate
react-day-picker override, and verbatim ui-tokens output.
* chore(ui): Apache-2.0 license, externalize @phosphor-icons/react, export barrel gaps
- License: Apache-2.0 (was MIT), LICENSE file copied from root and added
to the published files array, publishConfig.access set to public -
matches the packages/sdk precedent.
- @phosphor-icons/react moves from devDependencies to a regular
dependency and joins the Vite externalPackages list, so it's imported
rather than bundled - same treatment as @base-ui/react.
- Export CollapsibleProps and IconSwitchProps, which were already
reachable through the barrel but not exported as types.
- Extract tooltip's open/close delay constants and placement context
into tooltip-context.ts so the barrel export of tooltip.tsx no longer
leaks internals.
- Fix a stale "Overflow UI" reference in css-layers.md.
* chore(tokens): add lint/typecheck scripts, README, and CI coverage
packages/tokens had no eslint config, no lint/typecheck scripts, and no
README, and its checks weren't wired into pr-check.yml despite feeding
packages/ui's build. Add the non-React eslint re-export used by
packages/types and packages/execution-core, document the tokens.json ->
Style Dictionary -> --ax-* CSS pipeline, and fold lint/typecheck/test
into the existing UI CI job.
* docs: document packages/tokens in CLAUDE.md workspace tables
* chore(docs): remove orphaned apps/docs/src/generated/ui-api.json
Stale generated artifact from an earlier TypeDoc setup; unreferenced
anywhere in the repo and not produced by the current docs build.
* chore: sync pnpm-lock.yaml after rebase onto main
* fix(ui): require react 19 in peer range
* refactor(ui): rewrite built-css layer check on postcss
Replaces the hand-rolled CSS scanner with postcss (already a
devDependency). Also tightens the :root exemption: a top-level :root
block now passes only when every declaration is a custom property.
* fix(ui): export WithIcon from the barrel so TypeDoc resolves the icon prop
* fix(ui): pin the select caret to the trigger's far edge
* refactor(tokens): derive token-set paths once from a validated manifest
- config.ts names sets exactly as tokens.json exports them; buildManifest()
validates the config up front and lists the available keys on mismatch
- Style Dictionary runs with brokenReferences: 'throw' - a dangling token
reference fails the build instead of shipping literal {token.path} values
- dist/tokens.css inlines the primitives (no relative @import), so the file
survives being copied out of the package alone
* fix(ui): stamp the @layer order statement into every built stylesheet
In the built barrel, libInjectCss evaluates per-chunk CSS before the entry
CSS, so the order statement loaded last and the cascade inverted (ui.base
beat ui.component) for every consumer following the README import path -
secondary/ghost buttons rendered white-on-transparent. Duplicate statements
are no-ops, so stamping the statement into each dist stylesheet makes any
load order safe: barrel, subpath imports, and files copied out alone.
combine-css-bundle now also throws when dist/assets is missing instead of
silently skipping the published entrypoints.
* fix(ui): layer react-day-picker styles under ui.base
The calendar was the one component whose styles lived outside the layer
contract: react-day-picker's stylesheet shipped unlayered and our theming
had to stay unlayered too, winning by specificity. The stylesheet now joins
ui.base at import time (@import ... layer(ui.base)) and the theming moved
into @layer ui.component, winning by layer order alone.
* fix(ui): inject box-sizing only into known styling contexts
The plugin visited every rule, so @Keyframes steps got box-sizing too and
the declaration became part of the animation (animated values override
normal author styles while running). Injection now allowlists plain styling
contexts (media/supports/container/layer) and skips anything unknown - a
missed redundant declaration is easy to spot, a wrongly injected one is not.
vite-plugin-dts excludes root-level *.spec.mts: a spec escaping the filter
drags vitest's type graph into the ts program and stalls the build.
* feat(ui): extend the built-CSS guard with order, name, and surface checks
Three new checks close the guard's blind spots:
- every dist stylesheet must lead with the @layer order statement (the
inverted-cascade class was invisible to the old checks)
- only layer names declared in src/styles/layers.css may appear - a typo
creates an undeclared layer that silently wins the cascade
- every *.css entry in package.json exports must exist in dist, and no dist
stylesheet may use @import (self-containment)
The rdp- substring exemption is gone: react-day-picker styles are layered
now, so the guard runs with no exceptions. Shared FailureReport type, all
parsing through postcss.
* fix(tokens): reject duplicate config sets and match primitives by basename
Two latent traps from adversarial review: a set listed twice (or in both
groups) made two builds race for one output file, and the theme filter
matched primitive names as substrings of the whole source path - a set
whose kebab name appears in the directory prefix (e.g. 'Tokens' vs
./dist/tokens/) would have silently emptied every theme.
* fix(ui): close guard and plugin gaps found in adversarial review
Guard (check-built-css.ts):
- allowlist matches exact dist paths, not basenames - a component entry
named 'tokens' no longer inherits the token files' exemption
- exports check walks conditional-export objects, not just string targets
- layers.css is parsed for the @layer statement with a clear error instead
of an unchecked .first cast that crashed on a leading comment
- comments inside :root token blocks are no longer flagged
Plugin (postcss-box-sizing.mts):
- ancestor walk is properly typed (Document in the parent chain surfaced
TS2322 under strict) and the :root skip covers grouped/qualified selectors
- dropped the inert vendor-prefix stripper; spec grew to 10 cases
tsconfig now includes the root build .mts files and scripts/ so these files
leave the typecheck blind spot; specs and vite.config.mts stay out - with
allowImportingTsExtensions (needed for the config's .mts imports) or a
vitest-importing spec in the program, tsc stalls for minutes.
* docs(ui): record the box-sizing plugin decision
Decision log for keeping build-time per-rule injection over lint-based and
selector-based alternatives (box-sizing does not inherit; portals escape
subtree selectors; a global reset leaks onto consumer elements). css-layers.md
points at the mechanism; CHANGELOG no longer claims styles.css establishes
the layer order - every stylesheet carries it now.
* fix(ui): focus the field when clicking input and text-area wrapper padding
The padding sits on the wrapper, not the form element, so clicks in that
area never focused the field. Forward pointerdown to the element;
adornment clicks keep their own behavior.
* fix(ui): soften the modal exit with symmetric ease timing
ease-out on the exit transition drops most opacity in the first frames,
which reads as an abrupt vanish; the pre-migration modal used symmetric
ease. Applied to popup and backdrop in both directions.
* fix(ui): split build tooling into its own ts program
Extending tsconfig.json's include with root build files made every tool
that reads the package tsconfig (TypeDoc in the docs generator, and
vite-plugin-dts before it) pull them into its program - TypeDoc hung
indefinitely. The component program is src-only again; build files and
scripts typecheck through tsconfig.node.json, chained in pnpm typecheck.
* fix(ui): keep react-flow handle state rules content-box
The build injects border-box per rule, so hover/connecting states of the
opted-out handle silently shrank from 1rem to 0.75rem. State rules now
repeat the opt-out; the plugin spec documents the contract.
* fix(tokens): fail the build when set keys collide after file-name normalization
* fix(tokens): clean dist before building so renamed sets leave no stale output
* fix(ui): fail the combine step when dist/assets holds no component CSS
* feat(ui): ship variable defaults inside the ui.base layer
Unlayered :root defaults raced consumer overrides by load order: a lazily
loaded component stylesheet silently restored the default. The build now
wraps top-level :root blocks in @layer ui.base (new postcss plugin for
component CSS, copy transform for the generated token files), so any
unlayered consumer override wins by layer rules. check-built-css drops
its :root and token-file exemptions and also rejects var() calls whose
first argument is not a dashed ident (var(), var(no-dashes)).
* feat(ui): widen react peer range to ^18.0.0 || ^19.0.0
Base UI itself supports 17-19 and no React-19-only API is used; the ^19
peer was declared from the build target, not an actual requirement.
* fix(ui): close the modal on a backdrop click
Dialog.Popup carried a full-screen wrapper, so every click landed inside
the popup and Base UI never saw an outside press - the modal could only
be dismissed with the close button or Esc. The popup is now the dialog
window itself (centered, capped at the viewport with its own scrolling
content area), and the orphan .backdrop-close class is gone.
* fix(ui): raise the date-picker popover like select and menu
Its Positioner was the only one without the shared popup class, so a
calendar opened inside a modal rendered behind it - which the SDK papered
over with a global z-index override. The 4px sideOffset goes away with
it: the shared class already carries that gap.
* fix(ui): raise the modal backdrop above host panels
At z-index 0 the backdrop sat below the consuming app's own panels, so
they stayed undimmed while a dialog was open and absorbed the clicks that
should have dismissed it.
* refactor(ui): trim inline comments to the traps worth keeping
Cut prose that paraphrases the code or records why a change was made -
that belongs in the commit and the PR. What stays is the handful of
non-obvious constraints: react-day-picker's selection quirks, the spec
shape that stalls the dts build, the cascade rule the state selectors
depend on.
* fix(ui): stop the text area overriding its typography class
The `font` shorthand set the size too, so a text area given ax-public-p10
rendered at the inherited 16px instead of 12px - and the taller field
pushed the properties panel into scrolling. Only the family is inherited
now; size, weight and line height come from the typography class.
---------
Co-authored-by: Jan Librowski <jan.librowski@synergycodes.com>
Co-authored-by: librowski-synergy <librowski-synergy@MacBook-Air-librowski-synergy.local>
Co-authored-by: librowski-synergy <librowski-synergy@SC-HD9N5Y40T6.local>
* refactor: migrate consumers to @workflowbuilder/ui and fix Base UI API breaks
* fix(sdk): stop publishing unresolvable ui dependency + leaked types
The SDK bundles @workflowbuilder/ui and @base-ui/react into its dist, but
declared them as runtime dependencies - so a published @workflowbuilder/sdk
would 404 on @workflowbuilder/ui (not published) at npm install. Move both to
devDependencies (they are inlined, consumers don't install them).
Add @workflowbuilder/ui to the dts plugin's bundledPackages so its types are
inlined into dist/index.d.ts instead of leaking unresolvable
import('@workflowbuilder/ui') references into the public type surface. Verified:
no real ui/base-ui imports remain in dist/index.d.ts.
Remove the obsolete docs/overflow-ui.md (described the old external
@synergycodes/overflow-ui local-dev flow this migration replaces).
* fix(sdk): finish overflow-ui migration for files main added after the branch point
Rebasing onto current main pulled in commits landed after this branch
diverged (WB-339's use-on-connect.tsx, PR #48's language-selector spec,
PR #48's ai-studio undo-redo buttons) that still imported the retired
@synergycodes/overflow-ui package. Point them at @workflowbuilder/ui
like every other already-migrated call site, and regenerate the
lockfile for the fully rebased dependency graph.
* fix(sdk): externalize @base-ui/react and @phosphor-icons/react from bundle
@workflowbuilder/ui is bundled (not externalized) into the SDK, so
@base-ui/react was being inlined transitively into dist/index.js even
though the SDK never imports it directly. @phosphor-icons/react was
already a direct SDK dependency but was likewise getting bundled a
second time on top of the copy demo/ai-studio install directly.
Add both to the SDK's Vite external list and move @base-ui/react from
devDependencies to dependencies, mirroring the treatment already used
for i18next/jsonforms/immer/zustand and matching how packages/ui
declares them.
* fix(sdk): repoint dead modal CSS selectors at Base UI signal
The datepicker z-index fix (index.css) and the variable-suggestions
backdrop suppression (variable-text.module.css) both keyed off
`.base-Modal-root` and `.mantine-Popover-dropdown`, class names from
the retired MUI Base + Mantine modal/popover. Neither selector matched
anything after the Base UI migration, silently reintroducing both bugs:
a datepicker opened inside a modal rendered behind it, and the
variable-suggestions backdrop no longer disappeared when a modal opened
on top of it.
ModalProvider now toggles a `wb-modal-open` class on <body> whenever the
SDK's modal store has a modal open - a stable signal owned by the SDK
rather than coupled to @workflowbuilder/ui's internal markup. Both
selectors are repointed at it; the datepicker fix additionally keys off
Base UI's own `[data-open]` popup attribute, since the DatePicker's
popover class is a hashed CSS module class internal to
@workflowbuilder/ui.
* docs(changeset): clarify Base UI DOM changes and dependency shape
Update the move-ui-library-in-repo changeset: @base-ui/react is now a
regular dependency of the SDK rather than an inlined implementation
detail, and note explicitly that internal DOM structure and class
names of the bundled UI changed (MUI Base + Mantine -> Base UI), so
consumer styles or tests written against internal class names may need
updating. Stays a minor bump.
* fix(sdk): declare layer order in module stylesheets using ui.component
Three SDK stylesheets open @layer ui.component without declaring the
ui.base/ui.component order first. Module-graph order currently loads the
declaration earlier by luck; an import reorder or dynamic import would
register ui.component first and invert the cascade inside the ui layer -
react-day-picker's raw defaults would then beat the calendar theming.
Duplicate order statements are no-ops, so the prepends are free.
* fix(ai-studio): pin react-is to the react 19 line for recharts
Removing the overflow-ui/MUI subtree left react-is resolvable only from
stale transitive copies, and pnpm rewired recharts' react-is peer to
17.0.2 (supplied by a vitest transitive). react-is 17 does not know
React 19 element symbols, so isFragment() is always false and fragment
children inside charts would be silently dropped. Pinned in the catalog
next to react/react-dom (must move in lockstep) and declared by
ai-studio to steer the peer resolution.
* docs(sdk): shorten the modal-backdrop suppression comment
* fix(sdk): keep the modal dialog mounted so enter/exit transitions run
ModalProvider mounted Dialog.Root conditionally: it appeared with open
already true and unmounted on close, so Base UI's transition lifecycle
(data-starting-style / data-ending-style) never ran in either direction -
on every Base UI version. The root now stays mounted with open driving
it, and the last modal content is retained through the exit transition
because the store clears it on close.
* build: bump @base-ui/react to the 1.7 line
The 1.4.1 pin guarded against a modal-fade regression attributed to Base
UI 1.6; the fade was actually broken by the SDK's own conditional Dialog
mounting (fixed in the previous commit) on every version. Caret range in
the catalog restores dependency dedupe for npm consumers of the published
packages; the workspace stays locked to an exact version via the lockfile.
* perf(sdk): stop bundling @workflowbuilder/ui CSS twice
dist/style.css carried every ui rule twice: once through the explicit
@import of ui's index.css and once through the JS module graph (each
bundled ui chunk carries a libInjectCss-injected CSS import). The JS
graph vector stays - it also tree-shakes CSS of components the SDK does
not use; the redundant @import goes. Verified by marker counts halving
in the rebuilt bundle (typography 24 -> 12) and a visual pass on demo.
* docs: note that app builds consume the prebuilt packages/ui dist
* docs(sdk): extend the archival banner over the UI-library swap and fix token paths
The decision log's sections 3-4 describe the overflow-ui Vite alias and
import layout as current; the banner now covers the swap to the in-repo
library and corrects the disproven CSS-extraction claim. The token doc
pointed readers into a node_modules symlink instead of packages/ui/dist.
* docs(changeset): cover derived type shapes and the restored modal fade
* fix(deps): resolve app react from the catalog
A caret react next to catalog react-dom is the exact drift the catalog
comment warns about - React 19 requires the pair to match (React error 527).
* fix(sdk): layer the xyflow stylesheet and drop its duplicate JS import
xyflow CSS arrived through two vectors (a bare css @import and a JS-side
import in diagram.tsx), both unlayered. Once the ui library's port rules
moved into @layer ui.component, unlayered xyflow beat them and node ports
rendered as raw 6px dark dots. The stylesheet now joins the declared (and
previously unused) ext-lib layer, which the ui layer outranks; the
duplicate JS import goes away. Verified in demo: styled ports and the
node-as-port connection target both render from the layered rules.
* fix(sdk): declare a single top-level cascade layer in the stylesheet
UI chunk CSS arrives through the JS module graph and can parse before
index.css; its stamped statement then registered top-level ui first and
the SDK's reset/ext-lib/ui declaration could only append, flipping the
effective order to [ui, reset, ext-lib] - xyflow beat every component
style. Now every stylesheet opens with the identical
@layer ui.base, ui.component; statement, xyflow and the SDK resets join
ui.base, and no second top-level layer exists to race against.
Verified live in demo: only one order statement registers, xyflow's
handle defaults sit in ui.base and lose to ui.component, and an
unlayered :root override prepended before all component CSS still wins.
* fix(sdk): gate the modal portal out of server-side rendering
The always-mounted portal evaluated createPortal(..., document.body) on
every render, so SSR of a closed editor threw ReferenceError.
* fix(sdk): match the date-picker trigger override to the new markup
DatePicker renders Popover.Trigger as the button itself, so the
.date-picker > div > button override matched nothing and date/datetime
fields lost their 2.5rem trigger height.
* docs(ui): align base-ui claims with the ^1.7.0 dependency
README and CHANGELOG promised a validated 1.4.x pin and blamed later
versions for transition regressions; the regression was our conditional
mounting, and the actual dependency is ^1.7.0. Changeset records the
dependency-contract change.
* docs(sdk): drop dead promises and add an npm path to the token guide
README advertised a scroll-thumb-hover token that does not exist and
claimed the html/body/root 100vh sizing ships in the package. The token
guide only described monorepo paths and suggested editing generated dist
CSS. Both now describe what actually ships and how an npm consumer
overrides tokens.
* fix(sdk): drop the global popover z-index override
The rule flattened every open Base UI popup to 1002 while a modal was
open - including Select and Menu, which sit at 10000 - so the variable
suggestions panel at 3000 painted over an open dropdown. It also used
!important against the README's own contract and matched consumer
popups. The date-picker positioner now carries the shared popup class in
@workflowbuilder/ui, which is what the rule was working around; the body
class stays for the variable-suggestions backdrop.
* build(deps): pin base-ui to an exact 1.7.0
The keep-mounted modal depends on Base UI's transition lifecycle, and
the range ships to consumers - a caret would let a minor change it
without our test.
* refactor(sdk): trim inline comments to the traps worth keeping
* refactor(sdk): drop dead rules from the dynamic typed input stylesheet
The stylesheet declared .container--select twice; merge the two into
one rule and drop its > select child, which never matched since the
component renders the UI library Select. Remove the unused
.cursor-pointer and .reset-button classes, and a comment explaining
why .date-picker needs no descendant selector.
---------
Co-authored-by: Jan Librowski <jan.librowski@synergycodes.com>
Co-authored-by: librowski-synergy <librowski-synergy@MacBook-Air-librowski-synergy.local>
Co-authored-by: librowski-synergy <librowski-synergy@SC-HD9N5Y40T6.local>
) * feat(docs): add live @workflowbuilder/ui component gallery * docs: point UI-library references at @workflowbuilder/ui * feat(docs): expand UI Library into page-per-component reference * refactor(ui): export component prop types and add @default tags * feat(docs): generate UI Library props and CSS tables from source via TypeDoc * docs(ui): render examples in isolated fixed-size previews with card-style props/CSS tables Wrap every UI Library example in a shadow-DOM ComponentPreview so components are styled only by @workflowbuilder/ui (isolated from Starlight CSS), shown in a fixed 2:1 dotted preview box that matches the original Overflow UI docs. Collapse each example island to a single representative instance. Rework the generated Props and CSS-variable references from tables into card lists: props show a 'required' chip (required-first) instead of a line-wrapping '?' marker, with Type/Default/description rows; CSS variables group into Color/Size. Drop the now-unused example-frame styles. * docs(ui): add live previews to diagram-component pages Give the Edge, NodeIcon, NodeDescription and NodePanel pages the same shadow-DOM ComponentPreview the UI components use, rendering each as a standalone example (NodePanel compositions; EdgeLabel variants positioned relatively outside a canvas) for parity with the original Overflow UI docs. NodeAsPortWrapper stays props-only, matching the reference. Adds @phosphor-icons/react to the docs app for the example icons. * docs(ui): anchor the Status example to a positioned container Status is an absolutely-positioned corner badge; rendered standalone it had no positioned ancestor and floated to the wrong place. Wrap it in a relative box that stands in for the node/field it marks, so it sits in the top-right corner as intended. * docs(ui): @base-ui/react is no longer a peer dependency The UI Library overview told consumers to install @base-ui/react alongside the package. It is now a regular dependency that installs automatically; react and react-dom are the only peers. * ci(docs): build UI before docs in the deploy workflow @workflowbuilder/ui components and generate-ui-api.mjs's TypeDoc pass both need packages/ui/dist to exist; add a Build UI step ahead of Build docs, mirroring the existing Build SDK step. * fix(docs): harden the UI API generator and guard component coverage - Fix the generator's lint errors (renamed vars for clarity, imported node:process, top-level await with process.exitCode instead of process.exit/main().catch, matching tools/preflight.mjs's pattern). - Strip internal engineering notes (matching /missing token/i) out of CSS variable comments instead of rendering them on the public docs pages. - Treat an unresolved props type as fatal (process.exitCode = 1) instead of a warning, so a renamed/typo'd type can't silently ship an empty page. - Add collectVariantProps() to merge Button's discriminated-union variant props (Label/Icon/IconLabel) into one deduped table with per-variant notes, and switch the TypeDoc entry point from index.ts (resolve) to src/components (expand) so those variant-only prop types get full reflections. - Add a check-ui-component-coverage.mjs guard, wired into generate:ui-api, asserting every packages/ui/vite.config.mts componentEntries item has a matching COMPONENTS entry, so a new published component can't ship without a docs page. - Run generate:ui-api before astro check in typecheck, so the coverage guard and generator failures surface there too. The unrelated PropRow/CssVar -> PropertyRow/CssVariable renames in props-table.astro / css-variables-table.astro are a lint-driven cleanup in the same generator/docs-api area. * docs(ui): document Collapsible and Icon switch, align Styles guidance - Add a Collapsible page: intro, live example, Usage, a hand-authored Parts table for Collapsible.Button/Collapsible.Content (no exported prop type to key a generated table on, same precedent as NodePanel), and generated Props/CSS variables tables. List it in the components index. - Add an Icon switch section to the Switch page documenting IconSwitch (live example, Usage, generated Props/CSS variables tables). - Rewrite the overview's Styles section: importing from the package root auto-injects the layer order/reset/tokens setup, so only tokens.css is needed; the styles.css + tokens.css pair only applies to the per-component subpath-import path. Matches packages/ui/README.md and packages/ui/css-layers.md. Depends on the collapsible/icon-switch COMPONENTS entries and the Button-variant TypeDoc entry point switch landed in the generator commit. * fix(ui): correct segment-picker shape prop's @default doc tag The runtime default (shape = 'default' in the forwardRef destructure) was documented as @default '' in the TSDoc comment, which the UI Library docs render verbatim. * chore: sync pnpm-lock.yaml with workspace typescript resolution Resolves vite-plugin-dts, vite-plugin-svgr, and i18next/react-i18next to a single typescript@5.9.3 peer resolution instead of a stale mixed 5.6.3/5.9.3 set, matching --frozen-lockfile. * refactor(ui): drop redundant Partial around WithIcon in ModalProps * fix(docs): include shared prop types in TypeDoc entry points Helper types like WithIcon live in src/shared, outside the components entry tree, so they got no reflection and their members (Accordion's and Modal's icon prop) silently vanished from the generated tables. * fix(docs): load the ui stylesheet at document level for portalled previews Modal, Menu, Select, Tooltip and DatePicker portal their popups to body, outside the shadow roots that carry the preview styles - the popups rendered unstyled. The library CSS is fully layered with no reset, so loading it globally is safe for the Starlight theme. * docs: drop the stale import-order requirement from the ui setup page Every built stylesheet has carried the @layer order statement since the stamping fix, so 'import styles.css before any component' is no longer a correctness requirement; styles.css also ships no reset, only typography. * refactor(ui): export the full DatePicker props surface The docs generator reads the exported DatePickerProps, but the props the component actually accepts (value, defaultValue, placeholder, valueFormat, type, error) lived on an unexported local widening type - the generated table documented none of the props the page's own example uses. The widening moves into the exported type, TSDoc included. * fix(docs): document union and overload components through the variant merge SegmentPicker's discriminated union was flattened first-wins: the table claimed value is always required and typed defaultValue as never, contradicting the page's own controlled/uncontrolled prose. NavButton was generated from its flat base type, so children - the prop its page is about - never appeared. Both now list their variant prop types, and a variant's 'foo?: never' exclusion counts as the prop being absent there instead of polluting the merged type. * fix(docs): fail loudly on every silent-empty path in the ui-api pipeline Four holes of the same class - the tool swallowing a problem and shipping a plausible-looking page: - a moved/renamed component directory made extractCssVariables glob nothing and exit 0 (page claims 'no CSS variables') - an unknown slug in PropsTable/CssVariablesTable rendered the empty state instead of failing the build (typo = false page in production) - a first-party type hidden behind an unresolvable utility wrapper (Partial/Omit) dropped its props with no trace - the class that once got the library reshaped to suit the generator - findTypeByName picked the first of duplicate type names silently * ci: build the docs site on PRs that can break it Every docs gate (ui-api generator, component-coverage guard, astro page rendering) used to run for the first time at release-time deploy - a broken docs change merged green and surfaced weeks later as a red deploy. Path-filtered to docs/ui/tokens changes so unrelated PRs pay nothing. * docs(changeset): record the exported ui prop-type surface * docs: pin section index pages to the top of their sidebar groups The index pages sorted alphabetically inside their own groups (the 'UI Components' link sat 7th within UI Components). Order 0 plus an Overview label matches the plugins/nodes convention; link lists use exact component symbol names, and the section description gains sentence case. * fix(ui): export the NavButton variant prop types from the barrel The changeset announced them, but only NavBaseButtonProps left the package - the variant types lived in component files outside every barrel. Verified with a consumer-side tsc probe against dist. * docs(ui): add default tags for runtime defaults missing from the tables The generator reads defaults from JSDoc only; Tooltip placement, IconSwitch variant and EdgeLabel size/state/type had runtime defaults with no tag, so their table cells rendered empty. * fix(docs): stop marking variant-only props as globally required Absent variants were filtered out before the every() check, so SegmentPicker documented value (controlled) and defaultValue (uncontrolled) as simultaneously required - a call that cannot exist. Required now means required in every variant; the variant note says where a prop is required otherwise. * ci(docs): require an MDX page for every generated component entry The coverage guard only cross-checked vite entries against generator entries; an entry with no page rendering its slug passed silently. * ci(docs): widen the docs gate path filter starlight-typedoc reads packages/sdk sources, and root manifests plus the workflow file itself shape the build - none of them triggered the gate, so an SDK change could break the docs build unnoticed until deploy. * fix(docs): let oversized examples shrink or grow the preview stage The fixed 2/1 stage with overflow:hidden clipped wide examples (Snackbar) on narrow layouts. The ratio is now a preferred size: shadow-root children cap at the stage width and content restores the automatic minimum height. Also corrects the isolation comment - inherited typography crosses the shadow boundary by design. * docs: complete the stateful examples and describe Snackbar's scope Nine usage blocks called useState without importing it, so a copied example failed to compile. Snackbar now says it is purely presentational - no positioning, stacking, or auto-dismiss. * docs: align pages with what the code actually does NodeAsPortWrapper stretches the existing target handle's hit area and adds no highlight of its own - the page claimed the whole node becomes a highlighted port with no preconditions. The missing-token note now describes the comments as historical markers (several referenced tokens exist in the export today). Overview drops the 1.4.x pin claim, and the custom-node guide gains the missing ui package install step. * docs: fix the dead ui-library link and the headless heading /ui-library/ has no index page - the section entry is /ui-library/overview/. The 'Headless components' heading sat above a paragraph describing a fully styled library built on headless primitives. * fix(docs): scope and classify the generated CSS variable tables Subcomponents with their own page had their variables repeated on the parent: Button listed 17 of NavButton's, Switch 12 of IconSwitch's, and overriding them there does nothing. The Color/Size split now follows the value through the design tokens instead of guessing from the name, which put edge-stroke-width under Color and the snackbar status borders under Size. * refactor(docs): share the component list between generator and guard The guard regex-scraped COMPONENTS out of the generator source, so a change in quote style would yield an empty list and pass having checked nothing. Both now import the same module; the remaining scrape (the vite entry list, which is TypeScript) fails on an empty result. * style(docs): rename a loop variable flagged by the lint rule * fix(docs): follow interface references when collecting props The reference branch accepted only type aliases, so a prop type written as an interface would drop its members from the table with no warning - the entry point of the same function already handled both forms. * fix(docs): generate the last two hand-written API tables NodePanel had no generator entry: its CSS variable list was written by hand and covered 10 of the 20 variables in source. The useEdgeStyle parameter table was hand-written too, so its type is now exported and documented from source. An entry may carry dir: null when it documents an API but owns no stylesheet - the edge variables stay on the Edge page instead of splitting off to the hook. * docs: drop the private icons package from the NodeIcon example The snippet imported Icon from @workflow-builder/icons, which is marked private and cannot be installed from npm - a consumer copying it hits an unresolvable dependency. It now uses Phosphor, which ships with the package, and says that any icon library works. * fix(docs): say when a component forwards native attributes Input documented four props while its own example used placeholder, value and onChange - and no page said the component extends a native element's attributes. The generator now detects the forwarded element by walking the prop type (through Omit/DetailedHTMLProps wrappers and first-party aliases) and each affected page states it in one line, instead of the table listing ~280 DOM attributes or staying silent. * docs: say that example icons need installing The examples import @phosphor-icons/react. It is a dependency of the library, not something a consumer may import without declaring it - under pnpm's strict node_modules that import does not resolve. The install page now says so once, and the NodeIcon page points at it. * docs: drop the prose about icon libraries The example's import speaks for itself. * docs: drop the missing-token section The comments it describes are stripped from the generated tables and do not survive minification into dist, so a reader of the docs has no way to encounter them. * refactor(docs): trim inline comments to the traps worth keeping --------- Co-authored-by: Jan Librowski <jan.librowski@synergycodes.com> Co-authored-by: librowski-synergy <librowski-synergy@MacBook-Air-librowski-synergy.local> Co-authored-by: librowski-synergy <librowski-synergy@SC-HD9N5Y40T6.local>
* fix(execution-core): make failed runs close as Failed in Temporal
…Route node_started event through error policy (#79) * fix(execution-worker): assign event sequence numbers in the workflow * fix(execution-core): route a failed node_started through the error policy * fix(execution-worker): serialize event emits to preserve SSE cursor order * chore(tests): run tests recursively across all workspaces --------- Co-authored-by: Dawid Aksamski <dawid.aksamski@synergycodes.com>
* feat(execution-core): require start node * feat(sdk): add isStartNode node-data flag * feat(ai-studio): mark trigger as start node via isStartNode flag * feat(backend): detect workflow entrypoints via data.isStartNode * feat(demo): mark trigger as start node via isStartNode flag * feat(docs): mention isStartNode flag --------- Co-authored-by: Dawid Aksamski <dawid.aksamski@synergycodes.com>
* feat(execution-core): emit node_skipped events * fix(execution-core): tolerate node_skipped emit failure and emit skips in fatal waves * feat(execution-core): report error_route_not_taken for dormant error branches * docs(execution-core): add replay rule for emit-set changes, fix stale references * fix(ai-studio): use existing text token for muted colors, add skipped state to visualize card --------- Co-authored-by: Dawid Aksamski <dawid.aksamski@synergycodes.com>
* feat(execution-core): add execution_incomplete terminal state * fix(execution-core): treat falsy nextPort as no port in dead-end detection * refactor(types): single source of truth for terminal statuses and event types * fix(backend): close the snapshot-window and cancel races on terminal states * fix(execution-worker): refuse to overwrite a terminal execution status * refactor(execution-core): compute root liveness upfront and return the dead end from propagate * test(execution-core): cover the deleted-join-edge dead-end shape * docs(execution-core): correct dead-end causes and errorRoute absorption claims * refactor(ai-studio): derive execution status colors from semantic text tokens --------- Co-authored-by: Dawid Aksamski <dawid.aksamski@synergycodes.com>
#110) * feat(execution-core): add write-time secret redaction for event payloads * feat(execution-core): record step inputs on node_started with redacted payloads * refactor(execution-core): record visible node ids instead of output copies on node_started * docs(execution-core): replace ticket IDs in shipped comments with follow-up slugs * docs(execution-worker): cite Temporal blob-size warn limit --------- Co-authored-by: Dawid Aksamski <dawid.aksamski@synergycodes.com>
* feat(temporal): add @workflowbuilder/temporal plugin package Packages the Temporal integration as a Temporal Plugin instead of app-level wiring. Registers the three activities on SimplePlugin, ships the workflow runner under /workflow for the consumer re-export pattern, and moves the client engine to /client. Private packages (execution-core, types) are bundled into dist through a single seam file that imports them by relative path, so the emitted types stay self-contained. Ships with private: true until the first publish is approved. Worker and backend still run on their own copies of the workflow files; repointing them onto the package is the follow-up PR. * refactor(execution-worker): run the worker and backend on @workflowbuilder/temporal Replaces the hand-wired Temporal setup with the plugin. The worker now declares only what is its own (one executor per node type, the database as the store port) and hands the rest to WorkflowBuilderPlugin; workflows.ts re-exports runWorkflow so Temporal's bundler picks it up. The backend starts and cancels runs through TemporalWorkflowEngine. Deletes the files that moved into the package in the previous commit, so the workflow logic exists in one place again. The task queue name and the workflow id convention now come from the package instead of being repeated in both apps. No behaviour change: the same graph runner, the same activities, the same timeouts. * fix(ai-studio): keep the event stream open while cancelling a run Cancel closed the SSE connection before sending the DELETE, so the execution_cancelled event that the worker emits a moment later never reached the client. The panel stayed in 'running' with no terminal log line, even though the run had been cancelled correctly on the backend. Cancelling is asynchronous: the endpoint only asks Temporal to cancel, and the terminal event arrives over the stream afterwards. The stream adapter already closes the EventSource once it sees a terminal event, so dropping the early disconnect is all that is needed. * build(temporal): align the Temporal SDK packages on 1.23.0 @temporalio/plugin was added with a ^1.16.0 range and resolved to the latest release, while worker, client and workflow stayed pinned at 1.16.0 from the existing lockfile. That left the worker tested against a plugin seven minor versions ahead of it, which the catalog comment explicitly says must not happen. Raises the catalog and the published peer range to ^1.23.0 so the lower bound we ship matches the line we actually test on. @temporalio/plugin is worth watching here: it declares neither dependencies nor peers, yet its types come from worker, client and common, so a drift between them is invisible until something breaks at runtime. * docs: point references at the code moved into @workflowbuilder/temporal The refactor moved the workflow, the event emitter and the client engine into the package; the documentation kept pointing at their old paths. - replay-audit.md referenced the deleted run-workflow.ts, which matters because the package README's versioning section cites this document - the worker README described a file tree and a sandbox rule that no longer exist, and named TemporalEngine - the backend README pointed at the deleted temporal-engine.ts - execution-core's README and diagram still said TemporalEngine - drain-events.ts pointed at the emitter's old location DECISION-LOGS.md is updated by hand: the collector fails on apps/backend/tenant-context-port.decision-log.md, which uses a "Proposed / Landed" header instead of the "Date" the script expects. That break predates this branch. The missing entry for terminal-states.decision-log.md is added here too. * fix(temporal): name the plugin workflowbuilder.WorkflowBuilderPlugin The plugin registered itself as '@workflowbuilder/temporal', an npm package name, which matches neither Temporal's written standard nor what their shipped plugins do. This string appears in users' worker logs and is part of what Temporal reviews. Their Technical Review Standards ask for `my_library.MyPlugin` and every example in the plugins guide passes `organization.PluginName`, while their own @temporalio/interceptors-opentelemetry registers 'OpenTelemetryPlugin' and @temporalio/ai-sdk registers 'AiSDKPlugin'. The dotted form satisfies the standard and the docs at once, so it is the safer pick; a comment records why, and a test pins it so it does not drift back. * build(temporal): make @temporalio/plugin a peer dependency It was the only @temporalio/* package installed as a regular dependency, so it could resolve to a different version than the worker, workflow and client the consumer pins. Temporal releases these together and their types reference each other across package boundaries, so a drift surfaces as confusing type errors. @temporalio/plugin is the easiest one to get wrong: it declares no dependencies or peers of its own, yet its types come from worker, client and common. Nothing stops it from moving ahead on its own, which is exactly what happened here before the versions were realigned. The reference worker now declares it too, so the repo exercises the same install shape a published consumer gets rather than relying on the package's devDependencies. README documents the one-version rule. * docs(temporal): trim the plugin name comment to what protects the code The rationale for picking the dotted format over the bare class name belongs to the review thread, not to a comment that has to stay correct for years while Temporal's own plugins keep doing their own thing. What stays is what stops someone from breaking it: it is user-visible, Temporal reviews it, and it is not the npm package name. * fix(deploy): build @workflowbuilder/temporal for the production image The runtime stage installs with --prod, so tsup is absent and the package's `prepare` cannot build itself; .dockerignore also keeps dist out of the build context. Since backend and execution-worker now depend on the package, the image build failed outright on `tsup: not found`. Adds a package-build stage that installs with dev dependencies and builds the package, then copies only the resulting dist into the runtime stage. `prepare` is dropped for the package as well as the root, since the copied dist is what it would have produced. Nothing in CI builds this image, so this only surfaced when the image was built by hand. Verified by running both compose commands in the built image: each now fails on unreachable infrastructure rather than on module resolution. * build(temporal): declare the Temporal SDK packages the way Temporal's own plugins do Reverses the earlier move of @temporalio/plugin to peerDependencies, and takes client and workflow with it. The reason is mechanical: pnpm does not link peers into a workspace package's node_modules, because a package linked from the repo gets no per-peer variant. Locally the package's devDependencies hid this; a --prod install removes them and the built dist can no longer resolve its own imports. In the production image packages/temporal/node_modules/@temporalio contained only what the package declared itself. Each entry point imports exactly one @temporalio package at run time (plugin, client, workflow respectively), so those three are now dependencies. Both of Temporal's own plugins do the same: @temporalio/ai-sdk keeps client, workflow and plugin as dependencies, and @temporalio/interceptors-opentelemetry keeps plugin there too. @temporalio/worker stays an optional peer: no entry point imports it at run time, it is the consumer's process to create, and its native core-bridge is 146 MB, so a second copy would be expensive. The intent behind the review comment is kept — the SDK packages still cannot drift apart — but it is now enforced by pinning them here rather than by requiring the consumer to install a matching set. README documents what the consumer installs, and knip records why execution-worker keeps @temporalio/workflow: the workflow bundler resolves it from that workspace while compiling workflows.ts, even though no source file imports it. * ci: run the release workflows on Node 24 so the OIDC publish works The Trusted Publisher exchange is done by the bundled npm, and only npm 11.x (Node 24) performs it natively when no token is configured. release-sdk.yml was moved to 24 in fabf16c after the publish failed on authorization; the other two workflows were written against the older shape and kept Node 22. That went unnoticed because neither package has ever been published: the registry returns 404 for @workflowbuilder/ui and @workflowbuilder/temporal, so the publish step in those workflows has never actually run. The first real release would have failed on authorization. Also fixes release-ui.yml, which is outside this PR's scope but carries the identical defect and the same never-executed publish path. The comment records why the version is what it is, so it does not get levelled with the rest of the repo later.
* build: make the pre-commit prettier respect the root .prettierignore Every workspace re-exports the root lint-staged config, and lint-staged runs each one with that workspace as the working directory. Prettier resolves .prettierignore relative to the working directory, so from packages/foo it found nothing and formatted files the root ignore list excludes. apps/icons/src/utils/icons.gen.ts is in that list, is not gitignored, and was therefore being rewritten by the hook on every commit that touched it, while the CI format job left it alone. The two disagreed. Passing the path explicitly, resolved off the config module's own location, pins it to the root file from every workspace. * test(temporal): replay recorded histories through Temporal's own replayer Adds the repo's first live Temporal test harness. The determinism test in execution-core is re-execution equivalence: it runs runGraph N times against identical mocks and compares the call sequence. That proves a run is reproducible against itself, which is rules 1-8 of the replay audit, but it structurally cannot see rule 9. A change that is perfectly self-consistent and still strands every in-flight run passes it green. The new suite starts a real Temporal, runs a graph, then does three things with the result: counts scheduled activities per type, replays the history it just recorded, and replays a committed history from histories/. Only the last one catches cross-version drift, which is why it is the one that matters at review time. The graph is start to (left, right) to join. The fan-out is deliberate. It is the only shape that puts two commands in a single workflow task, which is where the runner's Promise.all becomes visible to Temporal. Verified by adding a fourth emitEvent to runGraph. The count assertion fails with emitEvent 11 against 10, and the committed-history replay fails with a DeterminismViolationError out of Temporal. The self-replay test stays green, as it should, because that history was recorded by the same modified code. Recorded histories are prettier-ignored so they stay byte-identical to what `temporal workflow show --output json` emits, keeping the two interchangeable. The audit's "Why no live runReplayHistory test" section is rewritten. The cost objection it recorded did not survive contact: no Java runtime, in-memory server, about two seconds. The real division of labour between the two forms is cross-version coverage, not primitive coverage. Tests, devDependencies and docs only, so nothing ships and no changeset is due. WB-527 * docs(temporal): say what a red cross-version replay means before the first release The replay rules were written as though the package had shipped: a failing history meant patched() or a major with a drain note. Neither applies today. The package is private with no published version, so no run recorded by an older build exists and nothing can be stranded. Splits the guidance by publication state. Before the first release a red test is a design signal, that a command reached a path meant to be left alone, and re-recording is a legitimate fix once the change is understood. After the first release it is a compatibility break and re-recording destroys the only evidence of what the published version did. Adds a triage list for reading the change, since only some edits move the command sequence. Both halves are verified rather than asserted: a stand-in for the durable-pause seam (an update handler registered unconditionally plus an unreached condition) replays the committed history green, while one extra emitEvent in runGraph fails it with a DeterminismViolationError. WB-527 * Apply suggestion from @librowski Co-authored-by: Jan Librowski <janlibrowski@gmail.com> * docs: shorten the root lint-staged comment to its reason The comment walked through how Prettier resolves .prettierignore against the working directory. That mechanism is recoverable from the two lines below it. The reason the path is pinned to the root file is not, so only that stays. WB-527 * test(temporal): trim replay test comments to what the code cannot show The replay harness landed with comments that largely restated the code under them: test names, assertion contents, a plain Promise.all, the shape already spelled out by the nodes and edges arrays. Those go stale the moment the file moves and add nothing at review time. What stays is the part the code cannot carry. Why Temporal replay covers ground that runner re-execution structurally cannot. That regenerating the committed history destroys the cross-version baseline. The wave order, which the runner derives topologically rather than reads from the fixture. Replaces the arithmetic note on EXPECTED_ACTIVITY_COUNTS with named constants, so the expression no longer needs prose to explain what its two 1s were. WB-527 --------- Co-authored-by: Jan Librowski <janlibrowski@gmail.com>
* feat(temporal): per-node-type activity options and node labels in Event History
Two changes on one piece of plumbing, which is why they land together. Node
activities were proxied once at module scope with a single blanket profile, so
neither a per-type timeout nor a per-node summary could be expressed. The proxy
now moves inside the runner port and is built per call from the node.
Event History listed a column of identical executeNode rows. Each node activity
is now scheduled with the node's authored label as its Temporal Summary, so the
history reads like the diagram it came from.
The label reaches the engine the same way errorPolicy and role already do: lifted
out of data.properties by the backend mapper onto BaseNode. It is lifted rather
than read out of config because an engine consumes it, which is the rule that
already governs that mapper. description, the other shared property, stays in
config since nothing reads it. Blank and non-string labels are dropped, because an
empty Summary renders as nothing where Temporal would otherwise fall back to
showing the activity type.
Per-type timeouts and retry caps arrive through createRunWorkflow on the /workflow
entry point rather than through plugin options. The TypeScript SDK compiles the
workflow bundle from the consumer's own workflows.ts, so the worker-side plugin
cannot reach into it; the profiles have to be declared where the bundle is built.
Re-exporting runWorkflow unchanged keeps today's behaviour, so the documented
one-liner still works and nothing needs migrating.
Profile entries are whole ActivityProfile values, not partials. A partial would let
a caller set a timeout and silently drop the retry cap, and what Temporal falls
back to is unlimited retries with backoff, which on a permanently failing model
call is an unbounded bill. Requiring both fields makes that a compile error.
resolveNodeActivityOptions is a pure exported function so the guardrail is testable
without a Temporal environment: a type with no entry must resolve byte-identically
to the old 10m/2 attempts. Its test also pins that a node type named `constructor`
or `toString` cannot pick up an Object.prototype member as activity options, which
a plain record lookup would have allowed.
Verified: 33 tests in the package, 99 in the backend, 146 in execution-core, lint
and typecheck clean across temporal, backend, execution-worker and execution-core.
The bundling test passing is the load-bearing one here, since it proves the factory
and the per-call proxy survive Temporal's own bundler.
The remaining half of the Summary task is the hero screenshot, which needs the live
stack and a human at the keyboard.
WB-523 WB-524
* docs: correct the backend and worker ready signals
The "Agent signals" table promised two log lines that no longer exist. The
backend logs `backend listening` with the url as a structured field, not
`Backend running on <url>`, and the worker logs `execution worker started` with
the queue as a field, not `Execution worker started on task queue: <queue>`.
Both are lowercase.
Found by using the table: a wait loop grepping the documented strings never
matched while both processes were up and serving. That is the exact failure the
table exists to prevent, and it is silent, since a missing ready signal is
indistinguishable from a slow start.
Also notes that these two go through a structured logger, so the message text is
the stable part and the trailing JSON fields are not.
* docs: stop adding changesets for @workflowbuilder/temporal until it ships
The package is still `private: true` at 0.0.0, so a changeset entry describes a
change against a version nobody ever installed. Wording that is correct for an
update ("now scheduled with", "keeps the previous behaviour", "no migration
required") reads as nonsense in a first release, and the release procedure already
has the maintainer rewrite the generated CHANGELOG by hand.
One changeset is queued to seed the first release notes. That single entry gets
expanded at publish time to describe the package as it ships, instead of the first
CHANGELOG being assembled from a development diary.
Worth stating explicitly because the tooling does not enforce this either way:
`private: true` blocks publishing, not versioning, so `pnpm changeset status`
lists the package regardless and an added changeset would be consumed normally.
The pr-check guard now skips while the package is private. Keying it on `private`
rather than a date or a comment means the first publish turns it back on by
itself, with no one having to remember.
* fix(temporal): validate node activity profiles eagerly and stop sharing mutable defaults
Four findings from a review pass over the previous commit.
**A malformed profile looked like a node failure.** proxyActivities was called inside
the runner port, so a bad profile threw at scheduling time, where runNode catches it,
emits node_failed, and hands it to the graph's errorPolicy. Under 'continue' the error
is absorbed into nodeOutputs and the run closes as completed: a typo in worker
configuration produced a green run with a node that never did its work. Profiles are
now checked once, as the workflow is built, and the message names the config path and
the offending node type.
The check has to be ours. Temporal's validateActivityOptions only asserts that some
timeout is present, so a duration like '30 minutes' passes it and fails later when the
command is built, and the retry shape is never validated at all.
**The defaults were shared, not copied.** The resolver spread the profile shallowly, so
the returned `retry` was the very object exported as DEFAULT_NODE_ACTIVITY_PROFILE.retry.
One assignment changed the retry cap for every node for the rest of the process. The
existing test asserted only `not.toBe` on the outer object, so its title promised an
immutability the code did not have. Both default profiles are now frozen including the
nested object, the resolver copies `retry` as well, and the test asserts the guarantee
its name claims. A caller-supplied profile is no longer aliased either.
**The blank-label contract lived in the wrong place.** Trimming and rejecting an empty
label was implemented only in the reference backend, but any consumer can build the
workflow input and this package is the published unit. The resolver now enforces it, and
ignores a non-string label rather than throwing on `.trim`. The backend keeps its own
guard: that one is about `label` on BaseNode staying clean for everything else that
reads a node.
**A test title had become false.** "maps data.properties to config verbatim" was already
approximate, since errorPolicy was lifted before this branch, and adding `label` made it
plainly wrong. Renamed to what it actually pins, with the lifted fields asserted.
Not fixable, documented in the README instead: a misspelled profile *key* cannot be
validated, because the workflow sandbox has no access to the executor registry. Such an
entry silently never applies and the node keeps the default profile.
WB-523 WB-524
* fix(temporal): make the profile validator agree with Temporal and with its own promises
Three correctness bugs in the validator added by the previous commit. None of them
affects this repo, whose worker uses the zero-config runWorkflow with an empty profile
map, and all three would have shipped to external consumers.
**The duration check disagreed with the type in both directions.** It rejected '1.5h',
which type-checks and which Temporal parses as 90 minutes, so a type-valid value threw
at runtime. It accepted '0s', '0m' and '00h', which the server treats as unset: the
command is refused, the workflow task retries forever, and the run sits in Running with
no terminal event. That is the exact failure mode the assert exists to prevent.
Decimals are now allowed and the value must be positive.
Worth stating plainly, because the type invites the mistake: `${number}` also admits
'-5m' and '1e3s'. The runtime narrows the format deliberately, so the error message and
the README now describe the grammar that actually survives rather than gesturing at
"a duration".
**The validation was not eager.** createRunWorkflow runs in the consumer's workflows
module, which Temporal's bundler keeps behind a lazy importWorkflows() called from
initRuntime per workflow instance. A bad profile therefore produced a green
Worker.create followed by every run wedging in a WorkflowTaskFailed loop, with no
execution_failed and no status change in the product's own database. The README
promised the opposite. assertNodeActivityProfiles is now exported so worker setup can
run it outside the sandbox, where a throw fails the deploy, and the README describes
what each call site actually catches.
**The exported resolver disagreed with the assert.** After Object.hasOwn, the
`?? DEFAULT_NODE_ACTIVITY_PROFILE` branch was unreachable on any path the assert had
seen, so `{ 'my/agent': undefined }` returned the default in a consumer's test while
production threw. Removed, so the two exported surfaces answer the same for the same
input.
WB-523 WB-524
* fix(temporal): describe every rejected profile, clamp the summary, check profiles at worker start
Polish pass over the profile validator before merge.
**An undefined entry threw a bare property-access error.** The resolver deliberately
does not default such an entry away, but it then dereferenced `profile.retry` and
produced "cannot read properties of undefined", naming neither the node type nor the
rule, while the map-wide assert named both. The per-entry check is now one function
called from both places, so the two cannot word the same problem differently.
**The Summary is clamped to 200 characters.** Nothing enforces a limit on this path
today: 3 MB summaries were observed going through unchallenged. But the server already
carries a 400-byte `userMetadataSummarySize` that newer validators read, and the label
is copied whole into every ActivityTaskScheduled event, so an unbounded display string
is worth capping either way.
Note the arithmetic, since the clamp is measured in characters: 200 of them stay under
400 bytes for Latin text, but not for CJK or emoji. Capping bytes would need a UTF-8
length in the workflow sandbox, which is a question worth answering only once something
actually enforces the limit.
**The plugin takes the profile map and validates it.** Passing it to
`WorkflowBuilderPlugin` makes a bad profile fail `Worker.create`, so a deploy fails
instead of every run wedging on first activation. Nothing on the worker side reads the
map, and the two sides are not linked automatically, so the README now shows one shared
constant handed to both rather than a separate hand-written assert.
The plugin also warns when a profile is keyed by a node type with no registered
executor. That is the one configuration mistake the sandbox genuinely cannot catch,
since the workflow has no access to the registry. A warning rather than a throw,
because one workflow bundle may serve several workers that each register a subset of
the node types.
Plus the README sentence that implied node.label populates itself: lifting it out of
the editor's properties belongs to whatever builds the WorkflowExecutionInput, and the
reference backend's mapper is named as the worked example.
* docs: write down what earns a code comment, and cut this branch to that bar
The rule was given twice in review and both times the code drifted back, so it now
lives in CLAUDE.md next to the other code-quality conventions rather than in anyone's
head. Two things earn a comment: what the code cannot show (external behaviour, a
constraint that lives elsewhere, a hazard with no visible trace) and a decision other
developers need so it does not get lost. One line by default, three as the ceiling.
Anything longer belongs in a README or a decision log, with the comment reduced to a
pointer.
Applied to this branch: 96 comment lines added, now 62. Nothing was deleted that
carried a fact, only the words around those facts. The zero-timeout server behaviour,
the Object.prototype hazard, the errorPolicy absorption and the reason the plugin
accepts a map it never reads all survive, in a third of the space.
Two removals worth naming. The blank-label rationale in the backend was a duplicate of
the same explanation in the package, where it belongs, and the function it sat above
says what it does in its name. The freeze note was floating between two constants and
now sits on the one it describes.
* fix(temporal): seal the profile map, clamp the summary by bytes, make the frozen defaults readonly
The Object.freeze on the two default profiles was invisible to TypeScript, because
the annotation erased the readonly modifiers. Tuning a default in place compiled
clean and threw inside the sandbox on first activation, wedging the workflow task.
The summary clamp counted UTF-16 code units against a byte cap: 200 characters of
CJK is 600 bytes, and a cut on an emoji left a lone surrogate that renders as
U+FFFD. It now normalises whitespace to one line, since Temporal renders the
Summary as single-line markdown, and clamps on code-point boundaries by byte.
createRunWorkflow now takes a validated, frozen snapshot of the profile map, so an
entry added or edited afterwards cannot reach the resolver unchecked. That makes
the per-node assert dead code, and one validation at one boundary replaces two at
different granularities.
Also: the plugin's profile-key warning goes through an optional logger rather than
straight to console, resolveExecutor no longer resolves a node type off
Object.prototype, and the concrete AI Studio node types declare the runner-level
fields the backend lifts instead of carrying them untyped.
* fix(temporal): gather the profile checks into one module and bound the retry cap to int32
Profile validation lived in three files, mixed with the resolver, the types and the
plugin. It is a designated growth area, since four wire-format checks are deliberately
deferred, so it now sits in one module whose header names what the boundary promises
and what it does not. The README carries the rest.
findProfilesWithoutExecutor moved with it and lost its logging. The module decides what
is wrong, the plugin decides how to report it, which also makes the warning testable
without spying on console.
maximumAttempts is now bounded to int32. Above that the proto field wraps: 2147483648
arrives as -2147483648 and 4294967296 as 0, and Temporal reads 0 as unlimited retries,
so an overflowing cap becomes its own opposite.
The summary clamp drops from 380 bytes to 300. 380 bytes of content serialize to a
409-byte payload against the server's 400-byte cap, because the margin was picked
without measuring the protobuf framing. The comment now states the reason that holds
whatever that cap turns out to be: the summary is copied into every
ActivityTaskScheduled event, so an unbounded one grows Event History for the life of
the run.
The profile-key warning claimed nodes of the misspelled type keep the default profile.
They get the custom one and then fail with no executor registered. It is the type you
meant that keeps the default.
* fix(temporal): put the duration bounds on the real cliffs and stop validating per node
The [1ms, 100d] window rejected values Temporal schedules happily. '365d' is a normal
startToCloseTimeout for a heartbeating activity, and '0.5ms' is half a million valid
nanoseconds. Worse, the comment and the error message blamed Temporal for a bound this
package had invented. Both ends now sit on what a protobuf Duration actually carries.
The floor is the more interesting half. A too-short but non-zero timeout fails loudly:
the activity times out, retries, and the run closes as failed with a legible message.
Only a zero duration fails silently, because the server reads it as unset and refuses
the command. So the floor only ever needed to exclude zero, which puts it at one
nanosecond rather than a millisecond above it.
The per-node assert added in the previous commit was the wrong shape for the right
goal. On the production path it was dead, since createRunWorkflow hands the resolver a
snapshot it already validated, and the only place it could ever have fired is inside
executeNode, where the graph's errorPolicy can absorb it into a completed run. That is
what the comment beside it warns against. The resolver splits in two instead: an
internal one that trusts the snapshot, and the exported one that validates the whole
map once and delegates. The descriptive message survives, the per-node cost does not,
and the README's advice is true again.
Also: the second bundling case produced a byte-identical module graph plus one string
literal, so it paid a webpack cold start for nothing; the reference worker passed a
logger the plugin could never reach without a profile map; and execution-core's README
now says why every lookup keyed by node.type goes through Object.hasOwn.
* refactor(backend): drop the conditional-spread trio for pickBy, and pin what it guarantees
Three lines of `...(x === undefined ? {} : { x })` where the name in the guard and the
name in the shorthand had to match, and a mismatch would have compiled. mapNode is also
about to grow a fourth lifted field, so the repetition was going to get worse rather
than stay put.
Verified rather than assumed: identical keys and identical JSON across every shape
(all present, one missing, none present), and the whole thing typechecks under strict.
remeda was already this repo's utility library in four workspaces.
The tests around it turned out to guarantee less than they looked. Only `label` pinned
that the key is absent; `role` and `errorPolicy` used toBeUndefined, which passes just
as happily when the key is present holding undefined. An errorPolicy the runner does
not recognise had no test at all, so nothing recorded that it vanishes entirely rather
than falling through to `config`. Mutating the mapper to keep undefined keys now turns
five tests red instead of two.
* docs(temporal): cut the claims that go stale, and the one the README could not keep
A reviewer pointed at a comment enumerating the runner-level fields on BaseNode, which
would have gone stale the moment a fourth arrived. The same shape turned out to be in
four other places: a comment in packages/types describing what the Temporal adapter does
with `label`, so the lower layer explained the upper one; a test comment pointing at a
file this branch had already moved half its tests out of; a measured ratio nobody would
re-measure; and a field description tied to today's single use of the logger. Each keeps
the reason and drops the detail, because reasons do not age.
The README carried a worse version of the same thing. It sent readers to
`apps/backend/src/domain/mapper/from-integration-data.ts`, and `files` publishes the
README, so anyone installing from npm was pointed at a path that does not exist for
them.
It also left readers with a false model of the duration grammar. Saying the bounds do
not narrow what Temporal accepts is true of the bounds and misleading overall, because
the grammar does narrow: Temporal parses with `ms` and takes '30 minutes' or '1 week'.
Somebody who knows that would have been refused with no explanation. Now it is stated.
Two smaller ones: a byte figure with no test behind it, and an appeal to how Temporal's
own AI SDK plugin works, which is a claim about code we do not control and nobody here
would notice changing. The profile-check section also lost its table and half its
length; the measured edge cases live in the follow-up task, not in the file someone
reads to start a worker.
* fix(temporal): stop forwarding unvalidated fields from a profile to proxyActivities
The validator checked two fields and the resolver spread the whole entry, so anything
else a caller's map carried went to proxyActivities unchecked. TypeScript does not catch
it either: excess-property checks only fire on fresh object literals, and a map built
from configuration is not one.
Where it lands is the problem. proxyActivities is called inside the runner port's
executeNode, so a throw from building the proxy or from invoking it is caught by the
graph runner and handed to the node's errorPolicy. Under 'continue' the run closes as
Completed with the node skipped. That is the exact failure the eager validation exists
to prevent, reached through the one door it was not watching.
The options object is now built from the two validated fields rather than spread, which
makes the path structurally impossible. On its own that would turn a wedged run into a
silently ignored setting, so the validator also rejects unknown keys, at both levels and
naming every one it found. Throwing rather than ignoring follows the same reasoning as
whole profiles over partials: two knobs are what this package deliberately exposes, and
a third is an API change rather than a typo.
Reproduced on dist before the fix, and all four new tests were checked by mutation:
restoring the spread reddens the resolver test, dropping the key check reddens the other
three.
…he throw site (#118) * feat(execution-core): add transient/permanent error classes, surface attempt in node_failed * build(temporal): add @temporalio/activity dependency * feat(temporal): map classified executor errors to non-retryable failures * fix(docs): trim error-classification comments to what the READMEs don't cover --------- Co-authored-by: Dawid Aksamski <dawid.aksamski@synergycodes.com>
… namespace/TLS/API-key auth (#113) * feat(backend): call any OpenAI-compatible endpoint from the AI adapt route * feat(execution-worker): boot without an LLM key; AI nodes fail at call time * feat(backend): Temporal namespace and TLS/mTLS/API-key connection config * feat(execution-worker): Temporal namespace and TLS/mTLS/API-key connection config * build(deps): put the AI SDK packages in the catalog * feat(deploy): pass LLM endpoint and Temporal connection config through the demo stack * fix(config): drop the OpenRouter key alias and built-in LLM defaults OPENROUTER_API_KEY is no longer read. As an unconditional fallback for AI_API_KEY it was sent as a bearer token to whatever AI_BASE_URL pointed at, so an old .env plus a repointed endpoint leaked the OpenRouter credential. There are no external deployments to keep compatible; rename the variable instead. AI_BASE_URL and AI_MODEL lose their code and compose defaults too, so nothing in the code points outside the network. The OpenRouter values live in .env.example only. AI is configured when all three AI_* vars are set; otherwise the worker boots and names the missing ones, AI Agent nodes fail with ai_not_configured, and the adapt route returns 501. * fix(execution-worker): make ai_not_configured a permanent failure Missing AI configuration cannot recover on retry, yet the plain NodeExecutionError was retried once and lost its code crossing the activity boundary — node_failed carried only the message. Thrown as PermanentNodeExecutionError it stops on the first attempt and the code survives via the classified-error envelope. Adds a test through a real Temporal dev server asserting the node_failed code, a single attempt, and the workflow's failure type; the unclassified path is pinned alongside as the contrast. * feat(deploy): pass Temporal TLS paths through compose and mount ./tls The apps already read TEMPORAL_TLS_CA_PATH / _CERT_PATH / _KEY_PATH, but compose passed none of them and the docs told users to edit the manifest. Both services now take every TEMPORAL_* variable from one shared YAML block, so they cannot drift, and mount ./tls (override via TEMPORAL_TLS_DIR) read-only at /etc/workflowbuilder/tls. The directory ships empty with a .gitignore so PEMs never reach git. * feat(deploy): let an external Temporal retire the bundled cluster Setting TEMPORAL_ADDRESS to an operated cluster or Temporal Cloud still started temporal and temporal-db, and the apps' depends_on edges let that unused stack block them. The bundled cluster, its volume, its debug UI and the start-order edges now live in docker-compose.override.yml, applied by default; COMPOSE_FILE=docker-compose.yml in .env leaves it out, so the apps depend only on app-db. The debug UI is documented as showing the bundled cluster only. * test(config): isolate env tests from the runner's environment loadEnv only stubbed the values a case supplied, so variables inherited from the shell leaked into the fresh module and cases asserting "unset" tested whatever the runner happened to carry. Every variable env.ts reads is now unset before each import, derived from the module's own keys so a new one cannot be missed, and restored afterwards. * docs(site): document secured and external Temporal configuration The standalone quick start covered AI_BASE_URL and keyless startup but none of the Temporal connection variables. Adds the namespace, TLS, API-key and mTLS table with the same semantics as the backend README, plus Temporal Cloud and private-CA examples. Also corrects the LLM section, which still described a built-in OpenRouter default, and moves the env snippets to the dotenv grammar the highlighter actually has. * fix(deploy): run compose from the project dir and ship both files to the VM The workflow drove a VM-local docker-compose.yml with -f, which disables the automatic override and ignores COMPOSE_FILE — after the bundled cluster moved into docker-compose.override.yml the demo VM would have run without Temporal. The deploy step now copies both compose files from the repo on every run, executes compose from /app/ai-studio, and passes the pushed tags as RUNTIME_IMAGE / WEB_IMAGE, so one compose file serves local builds and the VM. * fix(deploy): keep certificate files out of the image build context The build context is the repo root and .dockerignore excluded only .env files, so PEMs dropped into deploy/ai-studio/tls per the mTLS docs were copied into the runtime image by `COPY . .`. The directory is now excluded; the files reach the containers through the read-only mount only. * fix(config): ship .env.example with an empty AI_API_KEY The examples carried the placeholder `sk-or-...`, which envOptional treats as a configured key: a verbatim copy skipped the boot warning and sent requests to OpenRouter with a bogus token, surfacing a provider 401 instead of the documented ai_not_configured / 501 paths. The value is now empty and the key format lives in the comment. * docs: align the root README with the no-default LLM configuration The Full Stack Demo section still described AI_BASE_URL as defaulting to OpenRouter and listed a two-variable setup, contradicting the code and the docs site. It now mirrors the docs page: three variables, pre-filled by setup:env, no built-in default. * fix(deploy): refuse to start while OPENROUTER_API_KEY is still set Compose no longer passes the retired variable, so a pre-rename .env came up with AI silently off and only a warn-level log to explain it. A compose-level guard now fails interpolation with a message naming the rename and the two new variables; the README and .env.example carry the upgrade note. * docs: correct the Temporal failure mode and remove default-wording drift The troubleshooting row claimed the backend exits on a contradictory TEMPORAL_* setup; it connects on first use, so it boots, passes its healthcheck and fails on the first Play. Also aligns wording across the READMEs, .env.example files and docs page with the code: no built-in LLM default, any credential implies TLS, provider-neutral phrasing. * fix(deploy): keep custom TEMPORAL_TLS_DIR out of the image build context Only deploy/ai-studio/tls was dockerignored, so TEMPORAL_TLS_DIR=./certs with a key inside the checkout was copied into the runtime image by COPY . . on a local build. Exclude deploy/ as a whole (re-including only deploy/ai-studio/nginx, the one file the Dockerfile copies) and *.pem/*.key/*.crt/*.cer/*.p12/ *.pfx repo-wide. Document that TEMPORAL_TLS_DIR supports exactly ./tls or a directory outside the checkout. Verified with control files: none of the in-repo locations reach the build context; nginx/default.conf and a repo-root positive control do. * fix(ci): persist deployed image tags in the VM's .env The deploy step only exported RUNTIME_IMAGE / WEB_IMAGE for its own shell, so any later compose command on the VM (worker restart after a model change, the debug profile) fell back to the local ai-studio-* build names. Rewrite the two image lines in /app/ai-studio/.env on every deploy instead, leaving the rest of the file untouched, and drop the export so the deploy itself runs off the persisted values. Verified locally: after a simulated deploy against a stale .env, a fresh process with no inherited variables resolves both deployed tags via `docker compose config --images`; other .env lines and mode are unchanged. * test(backend,execution-worker): prove TLS, mTLS and API-key transport end to end The connection builder tests only compared the options object built from fake certificate bytes; nothing showed the backend's gRPC client or the worker's native transport would complete or refuse a real handshake. Add a shared test harness in apps/tools (throwaway CA with server and client leaves, a TLS-terminating proxy in front of the Temporal dev server, and an HTTP/2 endpoint that records bearer tokens) and drive both builders through it: private CA, mutual TLS, an untrusted server CA, a client certificate from the wrong CA, an API key inside the TLS session, and work in a non-default namespace. No Docker or Temporal Cloud needed. * refactor(temporal-connection): one copy of the TEMPORAL_* rules Backend and worker carried identical validation, TLS inference and certificate reading, kept aligned only by a "keep in sync" comment, plus duplicated defaults for the address and namespace. The option types were the excuse, but both SDKs accept a plain apiKey string and the same tls shape, so a narrow shared contract fits both uncast. Move all of it into a private source-only workspace, @workflow-builder/temporal-connection, behind a single temporalConfig() returning { connection, namespace }; each app hands the connection object to its SDK's connect call. Reading and validation still happen where they did: in the backend's first-connection factory and before the worker starts. An empty TEMPORAL_ADDRESS or TEMPORAL_NAMESPACE now falls back to the default like the other variables already did. The validation matrix lives once in the new package, and so do the TLS connection tests and their harness, a describe.each over both SDK transports; handing the built options to both connect calls there is the compile-time proof of assignability. The app suites are pure unit tests again and apps/tools is untouched. The workspace is added to the CI execution job's filter list, knip and CLAUDE.md. * refactor(ai-config): share the AI_* contract, keep the runtime reactions apart The rule that AI_API_KEY, AI_BASE_URL and AI_MODEL must be set together lived three times: the backend's adapt route, the worker's executor factory and its startup warning. Both apps also normalised empty values on their own. Add a private source-only workspace, @workflow-builder/ai-config, whose aiConfig() returns a complete config or the names of the missing variables, and make all three sites read it. What each app does when AI is unavailable is unchanged and stays in the app: the backend answers 501 after authorization and the guard, the worker boots and fails an AI Agent node with permanent ai_not_configured only when a run reaches it. The package README is the canonical description of the contract; both app READMEs and .env.example files point at it. The first-run guides named a template that does not exist and invited Play before the LLM section; they now say every bundled template has AI Agent nodes and what to expect without an LLM. * docs: limit the "no external traffic" promise to model requests Several docs said an internal AI_BASE_URL keeps all traffic in your network, or that nothing in the code points outside it. The optional web-search tool calls Tavily's API whenever TAVILY_API_KEY is set, a node enables search and the model invokes the tool, regardless of where the model runs. Say "model requests stay inside it" instead, note that the Tavily key must stay unset if nothing may call out, and remind readers that Temporal and the database go wherever their addresses point. Wording only; no behaviour changed. * refactor: drop a header that restated the function, name the PEM reader Remove the file header on the AI agent executor factory that repeated what its name and body already say. The comments explaining the real contracts stay: TEMPORAL_TLS is tri-state on purpose, and the AI configuration error is deferred to node execution so a keyless worker still boots. Rename the certificate reader readPemFile so call sites say what kind of file they read. * test(temporal-connection): remove the TLS test's temp PKI directories * docs(deploy): the backend calls the LLM too, for the visualize route * fix(deploy): check the retired key in the deploy script, not in compose * fix(backend): validate TEMPORAL_* at boot, like the worker * feat(ai-config): name a retired AI variable that is still set * test(backend): pin the env defaults the deploy depends on * test(temporal-connection): prove the plaintext default connects * fix(ai-studio): drop the provider name from the disclaimer
…ransient (#130) * fix(execution-core): make template resolution failures permanent resolveTemplate threw plain errors, so a malformed or unresolved reference was retried under the node profile even though a retried node receives the same context and fails identically. The three throw sites now raise PermanentNodeExecutionError with the codes template_malformed and template_unresolved, so the engine stops on the first attempt and the code reaches node_failed. * fix(execution-worker): make no_branch_matched a permanent failure A decision node with no matching branch was retried under the node profile, although the same inputs yield the same non-match on every attempt. The throw site now raises PermanentNodeExecutionError, so the node stops on its first attempt and the no_branch_matched code reaches node_failed for the first time: unclassified codes are dropped at the activity boundary. * feat(execution-worker): classify provider failures in the AI agent The AI agent rethrew whatever the AI SDK threw, so a rejected API key or a 400 burned every attempt the node profile allows. The catch block now maps a provider response by status, at the throw site and nowhere central: 401/403 and every other 4xx are permanent, 429, 5xx, 408 and a request that never got an answer are transient. Anything that is not a provider response passes through unclassified. The provider's own error stays attached as the cause, so node_failed keeps showing the provider's text; the HTTP status is in the message. The log line gains the code. The worker README documents the table. * test(temporal): cover a transient failure across the activity boundary The boundary test proved the permanent and unclassified paths but not the transient one. The new case runs a wrapped transient failure through a real Temporal dev server and pins that the node retries to the profile's limit, that node_failed carries the code and the attempt it died on, and that the deepest cause's message is what reaches it. * fix(execution-worker): read the provider status through an SDK retry wrapper classifyProviderError only matched a bare APICallError, so with SDK retries enabled every provider failure would arrive as a RetryError and fall back to the profile's uniform retry. The classifier now unwraps lastError first. Also: the status-less branch is reached for connection failures, not for a request that timed out, so the message and README row say so; the README no longer claims every failure is classified; the activity log line uses instanceof instead of a cast; the decision comment fits the three-line ceiling; the unclassified boundary case no longer borrows the no_branch_matched code this branch made permanent. * fix(execution-core): never report an empty message for a wrapped failure extractDeepestError returned the deepest cause's message verbatim. A failed fetch in Node ends in an AggregateError with an empty message, so a provider that refused the connection reached node_failed, and the AI Studio log panel, as a blank line. The walk now keeps the deepest non-empty message; everything else about the chain is unchanged. * test(execution-core): assert template failures with vitest matchers The template tests grew a bespoke catch-and-return helper next to twenty assertions written with toThrow; they now use toThrow with an asymmetric matcher like the rest of the file. The graph-runner test that models a template failure crossing an adapter builds the real PermanentNodeExecutionError and asserts the code it now carries. The comment above resolveTemplate restated the README and is gone. * docs(execution-worker): say what node_failed shows and record two deliberate choices The README claimed the HTTP status reaches node_failed. It does not: the event reports the deepest cause, so the provider's text is what the UI shows and the status lives only in Temporal's failure record. The paragraph now says so, states that 409 is permanent on purpose, and names the unparsable-2xx case as intentionally unclassified. Two comments that restated the READMEs or overstated the log line are trimmed to what the code cannot show. * test(execution-worker): share the provider error fixture and pin the 2xx fall-through The APICallError fixture was built in two test files; it now lives in one. The classifier table is a single list keyed by class, with the redundant classification literals dropped, and gains 409 plus the unparsable-2xx case that passes through unclassified. The comment about statusCode 500 moves back to the single-call test it describes, and the decision test asserts with toThrow matchers. * fix(execution-worker): stop telling decision authors to use an empty catch-all The no_branch_matched message advised adding a catch-all branch with no conditions, but a branch with no conditions never matches, so the advice reproduced the failure it explained. The message now asks for an always-true condition, the executor comment says why, and the test pins the wording. Making an empty branch the catch-all is a separate backlog item, marked with the follow-up slug in the comment. * fix(ai-studio): give the Support Triage template a working catch-all branch The "How-to / Other" branch had no conditions, which the decision executor never matches, so any ticket classified as neither billing nor bug failed the run with no_branch_matched. The branch now carries an always-true condition, the only form the executor treats as a catch-all today. * docs(execution-worker): point the catch-all comment at the default-branch follow-up The slug presumed that an empty branch will become the catch-all. The backlog task proposes an explicit default flag instead, so the marker now names the outcome neutrally. * fix(execution-worker): report the refused address when a provider is unreachable A refused connection reaches the SDK as an AggregateError with no message of its own, so APICallError renders it as "Cannot connect to API: " and node_failed showed a dangling colon. Only messages cross the activity boundary, so the classifier now picks the first AggregateError entry as the cause, where the array still exists. Also covers the README's unclassified promise with the real SDK classes, adds the untested RetryError and status-range fall-throughs, stops three tests using no_branch_matched as an unclassified example now that the code is permanent, and records the class change in the decision log.
…r path (#134) * test(temporal): record cross-version replay histories for every runner path Adds the fail-policy, incomplete-branch and cancel-mid-run scenarios next to parallel-wave, recorded from the harness rather than the UI so the inputs carry no secrets. The committed files replay through Worker.runReplayHistories, so one broken history no longer hides the rest, and UPDATE_REPLAY_HISTORIES now takes a scenario name so adding one does not re-baseline the others. * test(temporal): assert per-node events and make the history prefix an input The replay harness hardcoded the v0- prefix while the README promised <version>- recordings at the first release, so following it would have overwritten the baseline. REPLAY_HISTORY_VERSION now sets the prefix. Restores the per-node event shape and the terminal error message, which the scenario table had reduced to first/last event and status: the activity count alone cannot tell node_failed from node_skipped. Also trims scenario lists, guards against nested scenario names claiming each other's files, and narrows settle() to WorkflowFailedError.
Both app tsconfigs excluded spec and test files, so the pre-commit `tsc --noEmit` from lint-staged and `pnpm check` never saw them. CI does not type-check these apps by design (see pr-check.yml). The exclusion predates the first tests in these apps; removing it needs no code changes. `vitest/globals` joins `types` in both apps, as in the packages, so a spec that relies on `test.globals` type-checks too.
) deploy-docs.yml ran on every push to `release`, so merging a release PR for one package redeployed the whole site from that head. Between releases `main` runs ahead of npm: today it documents `isStartNode`, which no published SDK has, and tells readers to install @workflowbuilder/ui, which is not on npm. The workflow now runs only through workflow_dispatch, and its first step refuses a production deploy (SWA_ENV unset) from any branch but `release`, so a slip on the branch picker cannot publish `main`. Preview environments may still build from any branch.
…rep (#156) * chore(release): per-package release flow and temporal first-release prep Release one package at a time. `pnpm release:version <pkg>` computes the `changeset version --ignore` list from the workspace, so a release of one package never bumps its siblings on `release`. `pnpm release:tag <pkg>` creates and pushes exactly one scoped tag after checking that HEAD is the release head, the CHANGELOG has the section and no tag exists yet. `.changeset/config.json` trades the static `ignore` list for `privatePackages`, which is what frees the CLI `--ignore` flag. Prepare `@workflowbuilder/temporal` for its first npm publish: drop `private`, reset CHANGELOG.md to the bare heading so nothing leaks into release notes, trim the README to the npm landing page (the detail moves to activity-profiles.md and event-history-labels.md next to it, not linked until the docs site has pages), and document the manual first publish. RELEASE.md moves from packages/sdk to packages/ because it covers all three published packages; `@workflowbuilder/ui` is not on npm either and follows the same bootstrap. * refactor(release): split the release scripts into sections over a shared module Both scripts read as one block of statements with duplicated helpers and hand-rolled argument parsing. The plumbing (fail, run, package names, workspace listing, parseArgs) moves to tools/release-shared.mjs; each script is now a short linear flow under section banners with a one-line comment per block. release:tag prints each check as it runs instead of collecting them first. Behaviour is unchanged. * fix(temporal): declare the Temporal SDK packages as peer dependencies `@temporalio/activity`, `client`, `workflow` and `worker` were regular dependencies on `^1.23.0`, while a consumer's `@temporalio/worker` pins its siblings exactly. pnpm then installs a second copy of `@temporalio/activity` next to the worker's own, and `activityInfo()` reads an activity context that lives in the other copy. As peers they resolve to the consumer's single copy (verified in a scratch consumer: old shape two copies, new shape one). `@temporalio/plugin` stays a dependency, it has none of its own. Node floor raised to 20.3.0 to match the SDK. README lists the four packages to install and points at the two companion documents. * fix(release): harden the release scripts and cover tools/ in CI release:version refuses a changeset that names both a released and a skipped package, a release of the SDK while UI (compiled into it) has pending changesets, and any branch that is not `release-*` (the `release` branch blocks the `release/` namespace). It also stops when git cannot report the branch. release:tag checks the exit code of `ls-remote`, pushes with --no-verify so the pre-push formatter stays out of a release, and reads the tag back from origin before it reports. The CHANGELOG heading rule is one function with tests, mirrored by the awk in the release workflows: exact version token, so a prerelease never passes for the release. pr-check lints, formats and tests tools/, and its changeset guards count only changesets a PR adds, now also for the UI -> SDK pair. * docs(release): correct the procedure after review Release branches are `release-<pkg>-X.Y.Z`; the SDK/UI bundling pair and one-package changesets are spelled out; pushing to `release` deploys the docs; the trusted-publisher limitation cites its sources and asks for a check in the npm UI first; the UI CHANGELOG keeps only the heading above its first section so nothing leaks into release notes. * docs(release): record replay histories at release time, and fix stale claims The replay README requires every release of @workflowbuilder/temporal to record the scenarios again under the version it ships, and the first release to retire the `v0-` baseline; the runbook never said so, and its expected-diff list even contradicted it. Both now agree, and the replay README no longer describes the package as private or records under 1.0.0. The npm README gains the node-label sentence, a short "Failures and retries" section for the two error classes the changeset advertises, and the statement that the replay contract has no pre-1.0 exception. Corrected along the way: the lockfile is never touched by a bump (`workspace:*` carries no version), `pnpm publish` rewrites workspace devDependencies rather than stripping them, the breaking-change example points at the consumed changeset in git history, a test comment names activity-profiles.md, the SDK release title no longer repeats the package name, and CLAUDE.md speaks of all three published packages. * fix(release): fail closed on git errors and report npm honestly release:tag read a failed git command as an empty answer, which the checks took for a clean tree or a missing tag; the helper now stops the script with git's own message, except for the Actions link printed after the push. The npm check told "not on npm" for every failure, including a registry that could not be reached; it now distinguishes a published version, a missing one (empty answer or E404) and a failed lookup, and says which. release:version lists the replay-history recording among its next steps for the package that carries a replay contract, with the version already filled in, so the runbook and the tool name the same steps. * fix(release): verify the pushed tag, the plan and the notes; one release-notes rule release:tag now compares the sha of the tag on origin with HEAD after the push, so a tag of the same name that someone else pushed in between is reported as theirs instead of as a successful release, and it reads the CHANGELOG from the commit it is about to tag rather than from the disk. release:version refuses a dirty tree (its next step is `git add -A`), treats a `none` changeset as no release, and checks that the version on disk after the bump is the one the plan announced. The CHANGELOG section rule lives once, in tools/release-shared.mjs: a new `pnpm release:notes <pkg>` prints the section or fails when it is missing or empty, the three release workflows call it before the publish instead of carrying their own awk, and release:tag uses the same function. The workflows' idempotency check now treats only npm's E404 as "not published"; any other npm failure stops the run. pr-check also runs on PRs into `release`, its changeset guard reads only the front matter, tools/ is formatted and linted by lint-staged and `pnpm format`, and eslint gives tools/ Node globals instead of browser ones. * fix(temporal): every Temporal package is a peer, and replay recordings are never overwritten `@temporalio/plugin` was the one Temporal package left as a regular dependency after the others became peers, so a consumer pinned to worker 1.23 could receive any newer 1.x of an interface Temporal marks experimental. It is a peer now like the rest, execution-worker declares it, and the README lists the five packages to install at one version and tells a client-only backend to declare them too, since pnpm would otherwise pick its own. The replay harness refuses to overwrite an existing recording unless REPLAY_HISTORY_OVERWRITE=1 is set: recording at release time is routine now, and a run without REPLAY_HISTORY_VERSION would have silently rewritten the previous set and turned the cross-version guard into a self-check. Two comments and the catalog note now describe the dependency shape as it is. * docs(release): name the docs build source and drop the version from the UI README The docs site deploys from the merged `release` head, which is the snapshot of `main` from when the release branch was cut, not `main` at merge time; the runbook said otherwise. It also mentions that the replay harness refuses to overwrite recordings. The UI README no longer calls 2.0.0 the first release: the first version on npm will be whatever the pending changesets add up to. * fix(temporal): keep the Temporal peers out of devDependencies so --prod installs them The package listed its @temporalio/* peers in devDependencies as well. pnpm then counts each peer as satisfied and installs nothing for it, and the `--prod` install in deploy/ai-studio/Dockerfile strips the devDependencies, leaving the built dist with no @temporalio/client, plugin or workflow to import: backend and worker failed at startup, while every local install worked and pnpm printed no warning. With the five removed (`common` and `testing` stay, tests alone use them), pnpm installs the missing peers as ordinary dependencies of the package, which survive `--prod`. Verified on a replica of the runtime stage and of the package-build stage, and with the package's own typecheck, build and tests. CLAUDE.md and the catalog comment record the rule. * fix(release): count untracked files as dirty, read the workflow from HEAD, one npm rule release:version's clean-tree check skipped untracked files while its next step is `git add -A`, which adds them; the flag is gone and the message names them. release:tag checked the workflow file on disk while reading the CHANGELOG from HEAD; both now come from the commit that gets the tag. The shared npm rule loses a branch npm never takes (exit 0 with empty output) so it matches the release workflows. pr-check-docs also runs on PRs into `release`, the branch whose merge deploys the docs site. The tarball checklist names peerDependencies, and the replay README's pre-release path names the overwrite flag. * fix(temporal): declare @temporalio/worker as the test-only devDependency it is Only the tests import @temporalio/worker; src mentions it in two comments and dist never imports it. After the previous fix removed every @temporalio/* devDependency, the tests ran on pnpm auto-installing an optional peer, which pnpm only documents for required peers. The package now declares it in devDependencies next to common and testing, and keeps it as an optional peer for consumers that run a Worker. --prod still drops it from the package, which costs nothing because dist does not need it; verified on a replica of the Dockerfile runtime stage. knip flags every referenced optional peer regardless of devDependencies (it did on main as well), so the temporal workspace lists it under ignoreDependencies with the reason. CLAUDE.md and the catalog comment carry the refined rule, and the Code Quality table stops claiming that knip is part of `pnpm check`. * docs(release): the docs site deploys by hand from release deploy-docs.yml stops running on push to `release` (#164), so the runbook no longer says that merging a release PR deploys the site. Step 8 describes the manual run: from `release` only, once the site describes what is on npm, typically after an SDK or UI release; a release of a package the site has no pages for needs none.
`pnpm release:version temporal` consumed the one temporal changeset; the SDK and UI changesets stay queued. The generated CHANGELOG section is rewritten in Keep a Changelog form, with every symbol checked against the package's exports. The replay histories are recorded again under 0.1.0 and the pre-release `v0-` baseline is gone: from this version on, the four `0.1.0-*.json` files are the contract every change to the runner must replay. The replay README names the files by `<version>-` and explains where `v0-` went.
piotrblaszczyk
requested review from
librowski,
lukasz-jazwa and
szymon-t-sc
as code owners
September 21, 2026 12:54
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this is
Promotion of
maintoreleasefor the first npm release of@workflowbuilder/temporal(0.1.0, bumped in #165).mainhas been the only source ofreleasesince #165; this PR is that step.What
releasereceivesEverything on
mainsince the SDK 2.3.0 release: the temporal package and its release prep (#156), the manual-only docs deploy (#164), the version bump (#165), plus the SDK, UI and docs work merged in between. Only@workflowbuilder/temporalgets a tag; the SDK and UI keep their pending changesets and stay at their current versions.After the merge
release:pnpm publish --dry-run --no-git-checksinpackages/temporal, then the manual publish of0.1.0(npm cannot register a trusted publisher for a name that does not exist yet).release-temporal.ymland "Require 2FA and disallow tokens" on npm.pnpm release:tag temporal: the workflow sees the version on npm, skips the publish and creates the GitHub Release from the CHANGELOG.The docs site is not deployed by this merge (
deploy-docs.ymlruns by hand only) and stays as it is: the site still documents SDK 2.3.0, and the temporal package has no pages on it yet.