This release includes breaking changes for platform teams planning a safe upgrade.
✓ No known CVEs patched in this version
Topics
+8 more
Affected surfaces
Summary
AI summaryBroad release touches What's New, Fixes & Improvements, id, and blocks-antd.
Full changelog
What's New
feat: Add ajv-formats + ajv-keywords plugins, a compile({ schema }) export, and a ValidateSchema routine step
Packages: @lowdefy/api, @lowdefy/build, @lowdefy/ajv
Breaking change: @lowdefy/ajv now registers ajv-formats and ajv-keywords on the shared Ajv instance. Schemas that use format: date-time / email / uri / uuid / etc. or the instanceof keyword previously slipped through validate() un-validated; they are now checked. Schemas that were already invalid against these definitions will surface errors they did not before.
Additions
addFormats(ajv)— registers all standard JSON Schema formats (date,date-time,time,email,uri,uuid,regex,ipv4,ipv6, …).addKeywords(ajv, ['instanceof', 'transform', 'regexp'])— registers threeajv-keywordsextensions:instanceof— match JS class instances (e.g.{ instanceof: 'Date' }).transform— normalise string values during validation (transform: ['trim', 'toUpperCase']); mutates the parent object in place. Useful for upload pipelines that need cleaned values before downstream processing.regexp— full regex with flags (regexp: '/^l[0-9]+$/i'orregexp: { pattern: '...', flags: 'i' }); fills the gap left by JSON Schema'spattern:which has no flag support.
- New
compile({ schema })named export — returns a(data) => { valid, errors }function so callers can pre-compile a schema and reuse the validator across many calls without re-resolving throughAjv.prototype.validate.
Internal
- The configured Ajv instance is extracted into a new
src/ajvInstance.js. Bothvalidate.jsandcompile.jsshare it. - Plugin registration order is
ajv-formats→ajv-keywords→ajv-errorsso theerrorMessagekeyword can attach to format / instanceof errors.
ValidateSchema routine step (built-in)
A new connectionless server routine step (sibling to CallApi) that runs @lowdefy/ajv validate inside a routine. Properties:
schema— JSON Schema (required, operators evaluated).data— value to validate (required, operators evaluated).throwOnInvalid— boolean, defaulttrue. On invalid +true, the routine short-circuits withstatus: 'error'and the AJV errors attached aserror.cause. On invalid +false, the routine continues and the step result{ valid, errors }is available to downstream steps as_step.<stepId>.
Example:
routine:
- id: validate_input
type: ValidateSchema
properties:
schema:
type: object
required: [email]
properties:
email: { type: string, format: email }
data:
_payload: true
Wired through the existing build → runtime path used by CallApi: setStepId assigns a validate: id prefix, validateStep enforces required props and forbids connectionId, countStepTypes skips it, and runRoutine dispatches the prefix to a new handleValidateSchema handler in @lowdefy/api.
Use case
The hydra data-upload plugin uses compile({ schema }) to build a row validator from a tool's columns[] once per import and runs it against every row in the upload — pre-compilation avoids per-row dictionary lookup on large XLSX files.
feat: Add _app operator and structured app metadata.
Packages: @lowdefy/api, @lowdefy/build, @lowdefy/client, @lowdefy/operators, @lowdefy/operators-js, @lowdefy/server
A new runtime operator _app reads the app's declared metadata —
slug, name, version, description, license, lowdefyVersion,
gitSha. It works on both client and server, including inside
modules-mongodb request filters, and inside _js functions via a
bound lowdefyApp(p) callable.
The root lowdefy.yaml schema gains two new optional fields:
slug— a kebab-case identifier (^[a-z][a-z0-9]*(-[a-z0-9]+)*$),
validated at build time. Build fails with a clear error if invalid.description— a free-form string.
gitSha resolves through a fallback chain: LOWDEFY_GIT_SHA env var
when set non-empty → git rev-parse HEAD → null. This lets apps
deployed without .git (Docker, Vercel, Netlify, Render, hermetic
PaaS sandboxes) pin the SHA explicitly by mapping their platform's
commit env var via shell expansion in the build command.
Build emits a new appMeta.json artifact alongside app.json. The
existing app.git_sha field is removed; consumers (internal telemetry)
read gitSha from appMeta instead.
See the _app operator reference for the full key set and examples.
feat: Extend i18n coverage to Lowdefy agents.
Packages: @lowdefy/api, @lowdefy/build, @lowdefy/client, @lowdefy/blocks-antd-x, @lowdefy/server, @lowdefy/server-dev, @lowdefy/ai-utils, @lowdefy/helpers
Builds on the i18n / locale support from
feat-i18n-locale-support.md. End-user-visible strings in the agent
runtime and the AgentChat block now localize automatically when
config.i18n is configured.
Agent runtime errors. HTTP 4xx/5xx responses from the agent
endpoint (Only POST requests are supported., Invalid agent path,
Agent "X" does not exist., Agent type "Y" can not be found.,
Endpoint execution failed, etc.) translate per request via the
Accept-Language header against agent.runtime.* builtin keys.
AgentChat block UI. Framework-rendered strings in the chat UI go
through methods.translate against new agent.* builtin keys:
agent.sender.placeholder—'Type a message...'agent.toolApproval.{approve,reject}—'Approve'/'Reject'agent.message.{copy,feedback,regenerate,delete}— message actionsagent.toolResult.{completed,completedNoData,empty,emptyList,showMore,showLess}— tool result captions
Override per locale via config.i18n.messages.{locale} — same
mechanism as any other built-in message.
antd X locale wiring. The app shell now uses
@ant-design/[email protected]'s XProvider at the root (drop-in superset of
antd's ConfigProvider) with a merged antd + antd-X locale pack.
antd X ships only en_US and zh_CN packs; other locales fall back
to en_US for X-native strings ('New chat', 'Stop loading',
'Like'/'Dislike', bubble edit 'OK'/'Cancel'). Apps can
override these in unsupported locales via the new agent.antdx.*
reference keys.
Plugin-author surface. Agent hook endpoints (onStart,
onStepStart, onToolCallStart, onToolCallFinish, onStepFinish,
onFinish) now receive locale: <activeCode> in their payload, so
hook routines can branch on the user's locale.
System prompt translation. agent.properties.instructions passes
through the operator parser at request time — _t: works there for
locale-aware system prompts.
agents:
- id: assistant
type: AISDKAgent
connectionId: anthropic
properties:
agent:
model: claude-sonnet-4
instructions:
_t: agent.systemPrompt
What stays English (explicit choices):
- Built-in tool descriptions used in the model prompt (English-trained
models perform best with English tool descriptions). - Build-time agent validation errors (developer diagnostics).
- Console warnings (ops diagnostics).
- The
[File truncated — showing first NKB...]notice in the
read-filebuilt-in tool (model-facing). - Model-streamed natural-language output (owned by the model).
feat: First-class i18n / locale support for Lowdefy apps.
Packages: @lowdefy/api, @lowdefy/build, @lowdefy/client, @lowdefy/engine, @lowdefy/operators, @lowdefy/actions-core, @lowdefy/blocks-antd, @lowdefy/operators-js, @lowdefy/server, @lowdefy/server-dev, @lowdefy/block-utils, @lowdefy/helpers
Apps can now declare supported locales and message catalogs under
config.i18n, switch language at runtime, and translate their own
strings with ICU MessageFormat. Ant Design's component strings (date
pickers, modal Ok/Cancel, pagination, form validation messages),
dayjs date formatting, and the engine's built-in framework strings
(loading toasts, validation summaries, popup blocker warnings, error
page) all localize automatically once config.i18n is set.
config:
i18n:
defaultLocale: en-US
locales:
- { code: en-US, label: English, antd: en_US, dayjs: en }
- { code: de-DE, label: Deutsch, antd: de_DE, dayjs: de }
messages:
en-US: { greeting: 'Hello, {name}!' }
de-DE: { greeting: 'Hallo, {name}!' }
New schema — config.i18n with defaultLocale, locales[], and
messages. Validated at build time; only declared locales are bundled
(antd and dayjs locale imports are codegen'd, no ~150KB unused). The
missing-key fallback is always en-US, so plugin and module authors
should ship en-US translations as a baseline.
New operators
-
_t— translate operator with ICU MessageFormat. Resolution
order: active locale → fallback locale → built-in framework message
→ key._t: key: cart.items values: { count: { _state: itemCount } } -
_locale— readactive/default/fallback
(always'en-US') /supportedlocale state. Use withSelector
to build a language picker.
New action — SetLocale sets the user's preferred
locale (persisted to localStorage). Pass 'auto' to clear the
preference and fall back to the browser language or default.
Built-in framework strings. Engine and client strings ('Loading',
'Success', 'This field is required', validation summaries, popup
blocker, error page) live in a built-in catalog and surface as English
by default. Authors override per-locale by adding the same key to
config.i18n.messages:
messages:
de-DE:
engine.action.loading: 'Laden'
engine.validation.fieldRequired: 'Pflichtfeld'
See the Internationalization concept page for the full list
of overridable keys.
Ant Design block cleanup. Modal/ConfirmModal okText/cancelText
and date picker placeholders (DateSelector, DateRangeSelector,
DateTimeSelector, MonthSelector, WeekSelector) no longer hardcode
English defaults — they fall through to antd's ConfigProvider locale,
so a German app gets 'OK' / 'Abbrechen' / 'Datum auswählen'
without per-block configuration. The antd ConfigProvider block
itself now accepts a locale prop for subtree overrides.
Server-side translation. API requests resolve the user's active
locale from the Accept-Language header and thread it into the server
operator parser, so _t works the same in server-side actions and
requests as on the client.
Translation engine. A new translate() helper in @lowdefy/helpers
backs both the _t operator and the engine/client adapter (installed
on lowdefy._internal.translate). One source of truth for the lookup
chain; no duplication. Adds intl-messageformat as a foundational dep.
Plugin-author surface. Action and block plugins receive
methods.translate(key, values) and methods.getLocale() for runtime
translation in their JS code. Plugin packages can ship default
messages via a ./messages export — the build merges them into the
app's i18n catalog (user app messages > plugin messages > framework
builtins > key).
DatePicker and NumberInput auto-localization. Date selector blocks
(DateSelector, DateRangeSelector, DateTimeSelector,
MonthSelector) and NumberInput derive their default format /
decimalSeparator from the active locale via Intl.DateTimeFormat /
Intl.NumberFormat. A German user sees DD.MM.YYYY and 1234,56
automatically; an en-US user sees MM/DD/YYYY and 1234.56.
feat(api): Add callApi({ endpointId, payload }) to the request-resolver argument bag.
Packages: @lowdefy/api, @lowdefy/operators, @lowdefy/errors
Request resolvers (the JS resolvers shipped by connection plugins — e.g. plugin-http's get, plugin-mongodb's find) now receive a callApi function in their argument bag. Calling it invokes another Lowdefy endpoint in-process with the same semantics as the routine :call_api step: depth cap (10), caller's user identity, isolated routine context, inherited parser closure (_user, _secret, _env, _payload), and InternalApi endpoints reachable. Returns the target routine's response or throws on failure — UserError for :throw/:reject, original Lowdefy error class preserved otherwise.
Supporting improvements landed alongside:
_stateis now scoped to the routine frame.:set_statewrites no longer leak across routine boundaries. Two sibling:call_apiinvocations see independent state.UserErrornow accepts and forwardscause.controlThrow(:throw) andcontrolReject(:reject) constructUserErrorso routine-step and JS-boundary surfaces carry the same class for user-authored failures.callRequestResolverpasses all Lowdefy errors (those withisLowdefyError === true) through unchanged. Only raw errors are wrapped intoRequestError/ServiceError. A deepcallApichain no longer accumulates redundantcausenesting.runRoutineguards against doublehandleErrorinvocations when the same error crosses multiplerunRoutineboundaries on acallApichain.- The endpoint-invocation sequence (
depth check → load config → authorize → child routineContext → runRoutine) is factored into a sharedinvokeEndpointhelper used by both the routine:call_apistep and the newcallApifunction.
Behavior change: any app that accidentally relied on :set_state writes leaking across routine boundaries (e.g., a routine called via :call_api reading state set by its caller) will break. The leakage was a bug, not a contract — there is no backwards-compatibility shim.
feat(blocks-antd): Per-item styling and new props for menu items.
Packages: @lowdefy/build, @lowdefy/blocks-antd
Menu items in Menu and DropdownMenu now support the same class and slot-keyed style ergonomics as other Lowdefy blocks, plus the missing antd MenuItem props.
- Per-item
class(Tailwind / arbitrary CSS) onMenuLink,MenuGroup, andMenuDivider. Flat string/array applies to the item wrapper; objects with dot-prefixed slot keys (.element,.icon,.label, and.popuponMenuGroupfor the floating SubMenu popup) target specific parts. - Slot-keyed
styleon the same item types using.element/.icon/.label. Flat objects continue to work as a shorthand for.element. - New item properties:
properties.disabled(greys out the item and blocks clicks),properties.tooltip(text shown when the menu is collapsed — maps to antd'stitle), andproperties.extra(free-form right-aligned label on aMenuLink, e.g.beta,soon). shortcutbadge moved to the far right. The existingproperties.shortcutalready auto-rendered a kbd badge and wired the key handler — the badge is now floated to the far right of the item to match common menu conventions (previously inline next to the title). Whenextraandshortcutare both set on the same item,extrasits to the left of the shortcut badge.extrarendering note: rendered inside the<Link>viafloat: rightrather than antd'sextraprop. The antdextraprop triggers adisplay: inline-flex; width: 100%layout on.ant-menu-title-content-with-extrathat collapses Lowdefy-wrapped labels, so we bypass it.- Unified internals:
MenuandDropdownMenunow share one item builder, eliminating the prior divergence in icon CSS keys and which props were plumbed.
Block-level properties.theme on Menu is unchanged; pair it with properties.danger: true on a MenuLink to theme danger items via dangerItem* tokens. See the updated theming docs.
feat: Remove static exports declaration from modules.
Packages: @lowdefy/build
The exports: block in module.lowdefy.yaml is no longer required. Modules can now generate page, connection, and API endpoint ids dynamically — via _build.array.map, _module.var, or resolver functions — without declaring them upfront. Cross-module references are validated against the merged id sets after full resolve, with clearer, page-scoped error messages naming the broken reference and its source page.
A leftover exports: block has no effect on the build and is silently ignored. A codemod (modules-remove-exports) is provided to strip the dead field from your manifests.
feat: Plugin-driven serverExternalPackages for Next.js.
Packages: @lowdefy/build, @lowdefy/blocks-tiptap, @lowdefy/plugin-aws, @lowdefy/server, @lowdefy/server-dev, @lowdefy/server-e2e
Plugins can now declare which of their dependencies need to be passed
through to Next.js's serverExternalPackages config — used for CJS
packages whose runtime require() chains Turbopack can't resolve
through pnpm's isolated symlink layout (e.g. turndown →
@mixmark-io/domino, @aws-sdk/client-s3 → fast-xml-parser →
strnum).
Declare in the plugin's package.json:
{
"lowdefy": {
"serverExternalPackages": ["turndown"]
}
}
Build aggregates declarations from every plugin the app actually uses
(across blocks, connections, operators, actions, agents, auth, icons,
requests) and writes a per-app serverExternalPackages.json artifact,
read by server, server-dev, and server-e2e Next.js configs.
Replaces a hardcoded list in the three server configs. Apps not using
blocks-tiptap or plugin-aws no longer carry their externals.
Initial declarations:
@lowdefy/blocks-tiptap→turndown@lowdefy/plugin-aws→@aws-sdk/client-s3
feat(blocks-antd): Add a data-driven way to populate selectors, with valueKey and primaryKey.
Packages: @lowdefy/blocks-antd
Every selector — Selector, MultipleSelector, ButtonSelector, RadioSelector,
CheckboxSelector, SegmentedSelector and ListSelector — can now be driven two ways:
options(the original way): an array of primitives or{ label, value }pairs. Unchanged
and fully backward compatible.data+html: pass raw rows and render each option label with a Nunjucks template, instead
of building label/value pairs in your query.
New properties:
data— raw rows, an alternative tooptions.html— Nunjucks template for each option label (context:item,index).valueKey— field stored as the value. Withoptionsit names the value field (defaults to
value, so existing apps are unaffected); withdatait names the field stored on select (omit to
store the whole row).primaryKey— field used to match the current value (e.g. set withSetState) back to an option
for highlighting. Defaults tovalueKey.
Selecting an option stores valueKey's value, and setting that value (or array of values for the
multi-value selectors) via SetState highlights the matching option(s). Object options no longer
require a value field (relaxed to support valueKey). Adds the shared getSelectorOptions and
getSelectedIndex helpers; getUniqueValues/getValueIndex remain for PhoneNumberInput.
Every selector also gains a setData method (call it with CallMethod from the block's
onMount event, after a request loads the rows) to supply data imperatively. The dataset is held
in the block instead of properties, so the engine no longer re-parses and re-serializes every row
on each update cycle — keeping pages with very large selectors responsive. The selector still falls
back to properties.data/properties.options until setData is called.
feat(blocks-antd): Add ListSelector input block.
Packages: @lowdefy/blocks-antd, @lowdefy/helpers
Data-driven vertical list that doubles as a single-select input. Each item is rendered into a headerless antd Card whose body comes from a Nunjucks template against the row. Clicking a card sets the block value (the whole item, or the valueKey field when set) and highlights the selected card with a colorPrimary ring that follows the app theme. Clicking the selected card again clears the value (allowDeselect, on by default). Set selectable: false to turn selection off and render a read-only card list. Use valueKey to store a single field (e.g. id) as the value, and primaryKey (defaults to valueKey) to match a SetState-controlled value back to a card for highlighting.
Backed by react-virtuoso so thousands of variable-height cards render smoothly with only the visible window in the DOM, and rows are React.memo'd via a stable methodsRef so unrelated parent re-renders don't bust the cache. Selection state lives in the block value, so the selected card stays correct as rows scroll in and out of the virtual window.
Properties: data, html (Nunjucks), valueKey, primaryKey, selectable, allowDeselect, bordered, hoverable, size, gap, height, overscan, noData, theme, and an optional search object (placeholder, fields, caseSensitive, debounce, sticky, allowClear, minLength, noResultsText). Search defaults to matching every field path via JSON.stringify; supply fields: ['user.name', 'email'] to restrict. Filtering preserves the original index in the template context and event payloads. Built-in loading skeleton renders when loading is truthy. A text-only no-results placeholder appears when the filter matches zero items, and a noData placeholder renders in place of the list when the data array is empty.
Events: onChange ({ value, index, item }, fires on selection change when selectable is true), onClick ({ index, item }), and onSearch ({ value, resultCount }, fires on debounced query change when search is set).
@lowdefy/helpers: renames the built-in i18n message keys blocks.cardList.search.placeholder / blocks.cardList.search.noResults to blocks.listSelector.search.* to match the block, and adds blocks.listSelector.noData ("No data").
feat(blocks-antd): Rename TreeSelector → TreeInput, add TreeSelector and
Packages: @lowdefy/blocks-antd
TreeMultipleSelector dropdown blocks, and give all three the data-driven selector model.
The inline tree block (antd Tree) is renamed TreeSelector → TreeInput. Migration:
update type: TreeSelector → type: TreeInput in your YAML. TreeInput is now driven by the same
flat model as the other selectors — data + html + valueKey + primaryKey + parentKey (or
options) — and stores a single valueKey value (previously a root-to-node path array), matched
back to a node by value. It also gains a setData method. (Nested children options are no longer
used; build the hierarchy from a flat array via primaryKey/parentKey.)
New TreeSelector (single, valueType: any) and TreeMultipleSelector (multiple,
valueType: array) wrap antd TreeSelect as searchable dropdowns, sharing the same model. Build a
flat data/options array where each row's parentKey references the parent row's primaryKey;
the tree is assembled with treeDataSimpleMode. Selecting stores the valueKey value; a
SetState-controlled value highlights the matching node(s). showSearch, treeDefaultExpandAll;
the multiple variant adds checkable, showCheckedStrategy, maxTagCount. Both register a
setData method (call it from the block's onMount) for large trees.
All three blocks support antd design-token theme overrides, and the dropdowns localise their
placeholder / not-found text via the blocks.treeSelector.* / blocks.treeMultipleSelector.*
i18n message keys.
feat: per-option color on selector options.
Packages: @lowdefy/blocks-antd, @lowdefy/blocks-antd
Object options now accept a color, applied when that option is selected. It falls back to the block-level color when not set, and overrides it for that option when set.
- ButtonSelector — the selected option uses its own color:
solidfills the button (with auto-contrast text),outlinecolors the border/text plus a low-opacity tint. - CheckboxSelector / RadioSelector — each selected box tick / radio dot and its label render in the option's color (multiple checked boxes can each show a different color).
- Selector — the whole input is colored with the selected option's color. A
variantofsolidfills the input (with auto-contrast text);outlinedcolors the border/text. Dropdown options are tinted. - MultipleSelector — each selected value's tag/pill renders in the option's color, controlled by
variant:solid→ filled tags,outlined→ outlined tags (hex colors use auto-contrast text, dark-mode safe). An explicittag.colorstill takes precedence, and per-option tag colors no longer requirerenderTags.
Selector and MultipleSelector gain solid as a variant option (alongside the antd input variants). CheckboxSelector/RadioSelector have no variant — they only apply the per-option color.
- id: priority
type: ButtonSelector
properties:
variant: outlined
options:
- { label: Low, value: low, color: '#16a34a' }
- { label: Medium, value: medium, color: '#d97706' }
- { label: High, value: high, color: '#dc2626' }
feat: Tabs block now supports per-tab dynamic events.
Packages: @lowdefy/blocks-antd
Each entry in the Tabs tabs[] array can declare an eventName. When that tab becomes active, the named event is triggered (with { key } of the now-active tab) in addition to the generic onChange. This mirrors the per-button eventName pattern used by the AgGrid blocks and lets each tab run its own actions without branching inside onChange.
The Tabs event surface is onChange (fires on any tab change → { activeKey }) plus these per-tab events.
- id: settings_tabs
type: Tabs
properties:
tabs:
- key: profile
title: Profile
eventName: onProfileTab
- key: billing
title: Billing
eventName: onBillingTab
events:
onChange: # fires on any tab change → { activeKey }
- id: log_change
type: SetState
params:
activeTab:
_event: true
onProfileTab: # fires only when the Profile tab is selected → { key }
- id: load_profile
type: Request
params: get_profile
onBillingTab:
- id: load_billing
type: Request
params: get_billing
Label and input blocks no longer show an unwanted browser tooltip duplicating the label, which previously leaked raw HTML markup on hover when the label title contained HTML.
Packages: @lowdefy/blocks-antd
Tooltips are now opt-in via a new tooltip label property, which shows an icon beside the label with an accessible Ant Design tooltip. It accepts either a string (the tooltip text, supports HTML) or an object to also customize the icon and color:
label:
tooltip:
title: More information # supports HTML
icon: AiOutlineInfoCircle # defaults to AiOutlineQuestionCircle
color: '#1677ff'
A new onTooltipClick event fires when the tooltip icon is clicked. This applies to the Label block and all label-based inputs (ButtonSelector, Selector, CheckboxSelector, RadioSelector, TextInput, etc.).
feat(agents): persist full conversations, auto-mint conversation ids, and generate titles.
Packages: @lowdefy/blocks-antd-x, @lowdefy/ai-utils
- onFinish messages (fix): the agent
onFinishhook payloadmessagespreviously contained only the request input, so asave-conversationhook persisted the user's turns but never the assistant reply.handleAgentChatnow captures the final UI message list (input plus the generated assistant message, including tool parts) from the stream'sonFinishon both the default and prune paths, falling back to the input only when the stream errors or aborts. This repairs server-side conversation persistence. - generateTitle: new AISDKAgent
generateTitleboolean option. Whentrue, the first turn generates a short title from the first user message with a one-shotgenerateTextcall that runs concurrently with the response and emits adata-chat-titledata part, firing theAgentChatblock'sonTitleGeneratedevent. Off by default; failures are non-fatal. - conversationId minting: when no
conversationIdproperty is set,AgentChatnow mints a stable session id at mount and uses it for every request, so a turn sent before the app assigns an id (e.g. clicking a welcome prompt) no longer posts with an undefined id. The effective id is surfaced once per conversation, on its first user message, via a newonConversationStartevent. App-suppliedconversationIdvalues remain authoritative.
feat: Add _boolean operator.
Packages: @lowdefy/operators-js
A new operator _boolean coerces any value to its boolean truthiness,
equivalent to the _not of _not pattern or the JavaScript !!value
expression. It works on the client, server, and at build time, and
joins the existing type-cast operator family (_number, _string,
_array, _object).
See the _boolean operator reference for examples.
feat(e2e-utils): Support list-child blocks in ldf.block() and add ldf.list() helper.
Packages: @lowdefy/e2e-utils
Manifest now records list children under their templated id (listId.$.childId) by reading block category from plugins/blockMetas.json. At runtime ldf.block() falls back from the literal id to the template by replacing integer-only path segments with $, so ldf.block('legal_rows.0.toggle').do.toggle() resolves to the Switch helper without app authors writing raw page.locator(...). ldf.list(listId) adds .count(), .row(i), .rowBy(key, value) and .rowWhere(predicate) sugar.
Fixes & Improvements
-
fix(api,build): Render MenuDivider items in menus. (
@lowdefy/api,@lowdefy/build)MenuDivider items defined in a menu's
linkswere silently dropped at request time byfilterMenuList, which only letMenuLinkandMenuGroupitems through. Dividers now pass the filter and render via the existing Antd menu block code. A post-pass removes orphaned dividers (leading, trailing, or adjacent to another divider) so an item left dangling after auth-based filtering does not produce a broken-looking separator. ThemenuDividershape was also added to the build schema so configs containing dividers no longer trigger a schema warning, andbuildMenunow assignsauth: { public: true }to dividers for consistency with other menu items. -
fix(build): Resolve cross-module refs in module entry vars and connections. (
@lowdefy/build)Cross-module operators (
_ref { module, component },_module.pageId/connectionId/endpointId/id { module }) inside a module entry'svarsorconnectionsinlowdefy.yamlpreviously failed the build with "no module with that entry id was registered" because the entry subtrees were walked before any module was registered. They now resolve correctly against the app-level module registry — apps can compose components from one module into another's slot without forcing a dependency declaration on the host module.Behavior change: required-var validation now sees through
_refto the resolved value. A required var supplied via a_refthat resolves tonullpreviously passed validation (the raw_refobject was non-none) and now correctly fails. -
fix(build): Resolve
_refresolver and transformer JS paths against the module root. (@lowdefy/build)Modules can now ship their own JS resolvers and transformers. Previously, a
_ref: { resolver: resolvers/x.js }inside a module manifest failed withCannot find modulebecause the build resolved the JS path against the host app's config directory instead of the module root. Absolute paths inresolver,transformer, and.jscontent refs are also now honored verbatim. The existing package-root escape check that prevents module refs from reading outside their package is extended to cover both new fields. -
fix: Correct
secrets→secretin the server_jsfunction prototype. (@lowdefy/build)The generated
serverJsMap.jspreviously destructured{ secrets }, which
never matched the binding name (secret) passed at runtime by the_js
operator. As a result, any user_jsfunction that referencedsecretsin
its argument destructuring receivedundefined. The prototype now
destructuressecret, matching the runtime binding. -
fix: Unblock Playwright e2e for v5+ Lowdefy apps. (
lowdefy,@lowdefy/server-e2e,@lowdefy/e2e-utils)@lowdefy/server-e2enext.config.jsnow declaresturbopack: {}and drops the legacywebpackpolyfill block, so Next 16 (Turbopack-by-default) no longer errors withThis build is using Turbopack, with a webpack config and no turbopack config. ThetranspilePackageslist is now built from the samebuild/blockPackages.jsonartifact used by@lowdefy/server.- Plugin
typesmodules are now correctly unwrapped from their ESM default export, so apps using custom plugins (blocks, actions, operators, connections, requests, etc.) no longer fail withAction/Block/... type "Foo" was used but is not defined. - Plugin
blockMetasare now collected on the e2e server, matching the behaviour of@lowdefy/serverand@lowdefy/server-dev. lowdefy build --server e2eno longer crashes when the project has nolowdefy.yamlorlowdefy.yml(returns an empty plugin set instead ofYAML.parse(undefined)).- Page and API routes now use catch-all segments (
pages/[[...pageId]].js,pages/api/endpoints/[...endpointId].js,pages/api/request/[...path].js), so apps with nested page paths (e.g.pages: [{ id: 'foo/bar' }]) render correctly under--server e2einstead of returning 404. _app.jsand_document.jsnow mirror@lowdefy/server's dark-mode handling —useDarkModefrom@lowdefy/client, aThemeTokenResolverthat exposes the resolved antd token onlowdefy.theme._resolvedAntdToken, and a pre-hydration background-colour script that prevents the light/dark flash on page navigation.pages/api/client-error.jsnow enforces the same-origin host check and strips~e.receivedfrom incoming payloads, matching@lowdefy/server.lowdefy/build.mjsnow usesinstanceof BuildErrorfor the formatted-error shortcut (matches@lowdefy/server) and drops the obsoletemixinlogger config.- Runtime dependency set now includes
@lowdefy/blocks-antd-x, andtailwindcss/@tailwindcss/postcssare declared independencies(not justdevDependencies). The unusedprocessbrowser polyfill has been removed.
@lowdefy/e2e-utilsextractBlockMapnow traversesslots.<name>.blocksalongsideareas.<name>.blocksandblocks. Compiled page artifacts under.lowdefy/server/build/pages/<pageId>.jsonuse theslotscontainer shape, whichextractBlockMapwas not walking — sogenerateManifestproduced ablockMapcontaining only the page root andldf.block('<any-nested-id>')threwBlock "<id>" not found on page. Available blocks: <pageId>for every non-root block, reducing the e2e framework to root-block assertions and rawldf.page.locator(...)fallbacks.
lowdefyCLIlowdefy build --server <name>now re-fetches the server package when the version matches but the name differs. Bothlowdefy buildandlowdefy build --server e2ewrite to the same.lowdefy/server/directory, so the previous version-only cache check meant flipping between them (in either order) would silently reuse whichever server package was fetched first.
-
fix(blocks-antd): Display object-valued selector options correctly. (
@lowdefy/blocks-antd)Selectors whose value is an object (an option
value, or adatarow, that is an object rather than a scalar) now render the selected value again — both when an option is picked and when the value is pre-populated viaSetState(e.g. an edit form), including when matched byprimaryKey. Previously the value was stored correctly but no tag/value rendered.Selection matching and option de-duplication now compare values by identity — projecting
primaryKeywhen set, otherwise the whole value — and ignore build-time location markers, so an object value defined in config matches the same value arriving from state or a request. Scalar-valued selectors are unaffected. -
fix(blocks-antd): Correct nested
linksplacement in Menu meta schemas. (@lowdefy/blocks-antd)The Menu and MobileMenu meta schemas declared the third-level
linksarray inside the user-facingpropertiesfield instead of as a sibling of it. The schema shape now matches the runtime, so block-property validation and editor autocomplete report the correct structure for three-level menu configs. -
fix: PageHeaderMenu active menu item underline now sits exactly on the header's bottom border. (
@lowdefy/blocks-antd)The horizontal menu was vertically centered in the header, so the active item's underline floated half a pixel above the header's bottom divider — two disconnected lines under the selected tab. The menu now reserves the header's 1px bottom border when sizing its line box, so the active underline lands precisely on the divider, forming a single continuous line across the full page width (including the logo) and matching the single-border look of Sider / PageSiderMenu.
-
fix(blocks-tiptap): Fix broken image URLs when pasting or dropping images into the editor. (
@lowdefy/blocks-tiptap)Images pasted or dropped into
TiptapInputandTiptapMentionInputproduced a brokensrccontaining a double slash between the S3 bucket host and the object key, so the image failed to load. The uploaded image URL is now constructed correctly. -
fix(server-dev): Stop false "non-existent endpoint" warnings for CallAPI actions in dev. (
@lowdefy/server-dev)Under
lowdefy dev, everyCallAPIaction logged aConfigWarningthat its endpoint did not exist, even when the endpoint was correctly defined underapi:and worked at runtime. A fulllowdefy buildwas unaffected.The JIT page builder validates CallAPI references against
context.components.api, but the dev server's build context (getBuildContext) is rebuilt from disk artifacts and never restored the api endpoints, so the endpoint set was always empty and every reference was flagged. The dev context now hydrates the endpoints from the persistedbuild/api/<endpointId>.jsonartifacts (written by the skeleton build), so the non-existent-endpoint and InternalApi-not-client-callable checks behave the same in dev as in a full build.Module endpoints are written to nested paths (
build/api/<moduleId>/<endpointId>.json) because their scopedendpointIdcontains a/. The artifact reader now walks the api directory recursively so these endpoints are hydrated too; previously every module CallAPI was still flagged as referencing a non-existent endpoint.
Weekly OSS security release digest.
The CVE patches and breaking changes that affected production tools this week. One email, every Sunday.
No spam, unsubscribe anytime.
Share this release
About Lowdefy
Build internal tools, BI dashboards, admin panels, CRUD apps and workflows in minutes using YAML / JSON on an self-hosted, open-source platform. Connect to your data sources, host via Serverless, Netlify or Docker.
Beta — feedback welcome: [email protected]