Skip to content

Release history

Lowdefy releases

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.

All releases

9 shown

v5.3.0 New feature
⚠ Upgrade required
  • `@lowdefy/blocks-markdown` now declares `antd` (>=6) as a peer dependency; ensure it is installed in consuming projects.
  • PageHeaderMenu gains a default `borderBottom` for visual consistency—override via `styles.header` if needed.
Notable features
  • AI agents with `@lowdefy/ai-utils` runtime, multi‑provider connections (`anthropic`, `openai`, `google`, `ai-gateway`) and `AgentChat` composite block for streaming chat UI.
  • `blocks-aggrid` button cell renderer enabling per‑row action buttons with full antd Button configuration.
  • Ag‑Grid upgraded to v32.3.9, tag cells support arrays, auto‑colour by default, and suppressed cell focus/overflow clipping.
Full changelog

Highlights

  • AI agents are now first-class in Lowdefy. New @lowdefy/ai-utils runtime, a config-driven AgentChat composite block built on Ant Design X, and four new provider connections (@lowdefy/connection-anthropic, @lowdefy/connection-openai, @lowdefy/connection-google, @lowdefy/connection-ai-gateway) let you wire a streaming, tool-using AI agent into any page from YAML. Agents support multi-turn tool calling, server-side hooks (instructions, onStart, onStepStart, onToolCallStart, onToolCallFinish, onStepFinish, onFinish) callable as Lowdefy endpoints, sub-agents exposed as tools, file attachments with S3 upload, reasoning/thinking display, source citation streaming, and context compaction via pruneMessages. Provider-agnostic by design through the Vercel AI SDK.

  • AgentChat block: real-time streaming chat UI. Sequential message part rendering with configurable reasoning display, tool approval UI for endpoint and MCP tools marked confirm: true, file attachments (configurable accept types and max size, S3 upload integration), drawer display mode with a FloatButton trigger, source citation rendering, Mermaid diagrams, LaTeX, and syntax-highlighted code blocks (toggled via renderMermaid and codeHighlighter). Includes copy, feedback, regenerate, and delete message actions, plus suggestions and Sender.Header / Sender.Switch affordances. The new AgentConversations block provides a standalone conversations sidebar for independent placement.

  • MCP integration: agents can use external tools via Model Context Protocol. New @lowdefy/connection-mcp connection type supports HTTP, SSE, and stdio transports. Agents can reference MCP connections via connectionId or inline config with build-time validation. Runtime MCP client creation does automatic tool discovery, merging, and cleanup, and tool approval support via confirm: true works for both endpoint tools and MCP sources.

  • sharedState two-way binding: agents can read and write page state. The built-in update-page-state tool lets an agent write to the page state of the AgentChat block, so the agent can drive UI changes, fill forms, or read context the user has set elsewhere on the page.

  • File system agent tools. Sandboxed listFiles, readFile, searchFiles, statFile, and resolvePath tools give agents scoped access to agent-specific file directories. copyAgentFileSystems emits an agentFileSystems.json manifest so the production server includes each agent's fileSystem.basePath in Next.js file tracing, so agents that read files work on Vercel and standalone (output: 'standalone') deployments without manual next.config.js configuration.

  • blocks-aggrid: new cell.type: buttons renderer. Render a list of action buttons in a column with each button firing its own block-level event with the row data on the payload. Per-button properties mirror the antd Button block (title, icon, type, variant, color, size, shape, danger, ghost, hideTitle, disabled) plus row-data-path variants (titleField, iconField, disabledField, hiddenField) for per-row state. Use this for inline Edit/Delete/Approve actions without _if dispatching.

  • blocks-aggrid: ag-grid upgraded to v32.3.9. Pulls in two majors of upstream fixes. The column header UX (hamburger column menu with filter popup) is preserved by default; opt into the new ag-grid v32 column menu via columnMenu: 'new' on the block. Cell focus is suppressed by default (suppressCellFocus: true) so the keyboard focus outline doesn't visually compete with built-in cell renderers; override with suppressCellFocus: false. Cell overflow is clipped so flex-rendered content stays inside its column.

  • blocks-aggrid: tag cells render one tag per item for array-valued fields. The cell.type: tag renderer now accepts an array of strings in addition to a single string. Each item is rendered as its own styled tag and resolves its colour through the existing colorMap / colorFrom / default configuration. Empty arrays and arrays containing only null/empty entries render the em-dash placeholder. Single-string values are unchanged.

  • blocks-aggrid: tag cells auto-colour by default. When a cell.type: tag column is used with no colorMap, colorFrom, or default, tag values are coloured from a stable hash so the same value always gets the same colour across rows, columns, and tables. The palette uses 12 antd named hues (red, volcano, orange, gold, yellow, lime, green, cyan, blue, geekblue, purple, magenta) and respects the active theme. Opt out with cell: { type: tag, default: default }.

  • blocks-antd: PageHeaderMenu now has a default borderBottom, matching the existing default borders on PageSiderMenu and PageSidebarLayout, for visual consistency across the page menu blocks. The default can still be overridden via styles.header.

  • blocks-markdown: antd declared as a peer dependency. MarkdownWithCode imports antd for theme-aware syntax highlighting but the package did not declare it in peerDependencies, causing module resolution failures when bundling apps that include @lowdefy/blocks-markdown without already pulling in antd. Now declares antd (>=6) as a peer dependency.

Changes

feat: Add AI agent support with multi-provider chat and tool use

Packages: @lowdefy/api, @lowdefy/build, @lowdefy/blocks-antd-x, @lowdefy/connection-ai-gateway, @lowdefy/connection-anthropic, @lowdefy/connection-google, @lowdefy/connection-mcp, @lowdefy/connection-openai, @lowdefy/server, @lowdefy/server-dev, @lowdefy/ai-utils

Agent Runtime (@lowdefy/ai-utils)

  • handleAgentChat orchestrates the full agent lifecycle: tool merging, MCP client lifecycle, hook callbacks, and stream composition
  • ToolLoopAgent handles multi-turn tool calling, streaming responses, and artifact cleaning
  • createAgentUIStreamResponse converts agent output to a streaming HTTP response for the client
  • buildAgentTools merges endpoint tools, MCP tools, and sub-agent tools into AI SDK tool objects
  • buildPrepareStep enables dynamic tool phasing per step
  • buildUpdatePageStateTool provides a built-in tool for the agent to write to page state via the AgentChat block
  • File system agent tools: listFiles, readFile, searchFiles, statFile, resolvePath for sandboxed access to agent-scoped file directories
  • pruneMessages for context compaction
  • experimental_repairToolCall integration
  • Sub-agent support — agents can be exposed as tools to other agents
  • Reserved tool name collision detection (e.g. update-page-state)
  • Server-side hooks (instructions, onStart, onStepStart, onToolCallStart, onToolCallFinish, onStepFinish, onFinish) callable as Lowdefy endpoints
  • Provider-agnostic design using the Vercel AI SDK — supports reasoning/thinking display, providerOptions passthrough, and source citation streaming via sendSources
  • Strip data: URL prefix from file attachments before AI SDK processing

AgentChat Block (@lowdefy/blocks-antd-x)

  • New AgentChat composite block built on Ant Design X with real-time streaming display
  • Sequential message part rendering with configurable reasoning/thinking display
  • Tool approval UI for endpoint and MCP tools marked confirm: true
  • File attachment support (configurable accept types and max size) with S3 upload integration
  • Drawer display mode with a FloatButton trigger for embedding chat on any page
  • Source citation rendering for source-url and source-document parts
  • Mermaid diagrams, LaTeX, and syntax-highlighted code blocks (with copy + language label) — toggled via renderMermaid and codeHighlighter
  • Copy, feedback, regenerate, and delete message actions
  • Suggestions and Sender.Header / Sender.Switch UI affordances
  • Configurable roles, avatars, and names per message role
  • Event bridging for agent lifecycle events (onSuccess, onError, onFinish, onFeedback)
  • sharedState two-way binding lets the agent read and write page state via the update-page-state tool

AgentConversations Block (@lowdefy/blocks-antd-x)

  • New standalone conversations sidebar block, extracted from AgentChat for independent placement

Connection Plugins

  • @lowdefy/connection-anthropic: Anthropic connection with AnthropicAgent resolver supporting Claude models
  • @lowdefy/connection-openai: OpenAI connection with OpenAIAgent resolver supporting GPT models
  • @lowdefy/connection-google: Google AI connection with GeminiAgent resolver, including thinkingConfig and safetySettings sugar props
  • @lowdefy/connection-ai-gateway: Vercel AI Gateway connection with AIGatewayAgent resolver for routing to multiple providers through a single endpoint

MCP Integration (@lowdefy/connection-mcp, @lowdefy/ai-utils, @lowdefy/build)

  • New Mcp connection type for HTTP, SSE, and stdio transport config
  • Agents can reference MCP connections via connectionId or inline config with build-time validation
  • Runtime MCP client creation with automatic tool discovery, merging, and cleanup
  • Tool approval support via confirm: true on both endpoint tools and MCP sources

Build Pipeline (@lowdefy/build)

  • buildAgents validates agent config (model, tools, sub-agents, MCP) and normalizes tool definitions
  • writeAgents writes agent artifacts for server consumption
  • Sub-agent circular reference detection
  • Tool object format with confirm support
  • MCP connectionId normalization (inline config vs reference)
  • Lazy module variable resolution for agent properties referenced from modules
  • Agent schema validation integrated into the build pipeline
  • copyAgentFileSystems emits an agentFileSystems.json manifest so the production server can include each agent's fileSystem.basePath directory in Next.js file tracing — agents that read files now work on Vercel and standalone (output: 'standalone') deployments without manual next.config.js configuration

API (@lowdefy/api)

  • Agent route handler (callAgent) for streaming agent responses
  • Endpoint tool execution context with operator evaluation
  • Sub-agent resolver methods for agents-as-tools
  • MCP connectionId resolution at request time
  • getAgentConfig and getAgentResolver helpers for runtime agent resolution

Servers (@lowdefy/server, @lowdefy/server-dev)

  • Agent API route (/api/agent/[...path]) added to both production and development servers
  • urlQuery validation
  • 10 MB request body limit for file attachments
  • Server-side hooks for agent lifecycle callbacks (instructions, onFinish)

feat(blocks-aggrid): Buttons cell renderer and ag-grid v32 update.

Packages: @lowdefy/blocks-aggrid

New cell.type: buttons renderer — render a list of action buttons in a column with each button firing its own block-level event with the row data on the payload. Per-button properties mirror the antd Button block (title, icon, type, variant, color, size, shape, danger, ghost, hideTitle, disabled) plus row-data-path variants (titleField, iconField, disabledField, hiddenField) for per-row state. Use this for inline Edit/Delete/Approve actions without _if dispatching.

ag-grid updated to v32.3.9 — pulls in two majors of upstream fixes. The column header UX (hamburger column menu with filter popup) is preserved by default; opt into the new ag-grid v32 column menu via columnMenu: 'new' on the block.

Cell focus suppressed by defaultsuppressCellFocus now defaults to true so the keyboard focus outline doesn't visually compete with built-in cell renderers (tags, buttons, links). Override with suppressCellFocus: false if needed. Cell overflow is also clipped so flex-rendered content stays inside its column.

feat(blocks-aggrid): Tag cell renders one tag per item for array-valued fields.

Packages: @lowdefy/blocks-aggrid

The cell.type: tag renderer now accepts an array of strings in addition to a single string. Each item is rendered as its own styled tag and resolves its colour through the existing colorMap / colorFrom / default configuration. Empty arrays and arrays containing only null/empty entries render the em-dash placeholder, matching the existing null-value behaviour. Single-string values are unchanged.

feat(blocks-aggrid): Auto-colour tag cells by default for consistent per-value colouring.

Packages: @lowdefy/blocks-aggrid

When a cell.type: tag column is used with no colorMap, no colorFrom, and no default, tag values are now coloured from a stable hash so the same value always gets the same colour across rows, columns, and tables. The palette uses 12 antd named hues (red, volcano, orange, gold, yellow, lime, green, cyan, blue, geekblue, purple, magenta) and respects the active theme.

The grey fallback is still available — set cell: { type: tag, default: default } on any column to opt out. When colorMap, colorFrom, or default is set, behaviour is unchanged.

Fixes & Improvements

  • fix(blocks-antd): Add default header border to PageHeaderMenu. (@lowdefy/blocks-antd)

    PageHeaderMenu now has a default borderBottom matching the existing default borders on PageSiderMenu and PageSidebarLayout, for visual consistency across the page menu blocks. The default can still be overridden via styles.header.

  • fix(blocks-markdown): Declare antd as a peer dependency. (@lowdefy/blocks-markdown)

    MarkdownWithCode imports antd for theme-aware syntax highlighting but the package did not declare antd in its peerDependencies, causing module resolution failures (e.g., when bundling apps that include @lowdefy/blocks-markdown without already pulling in antd). Added antd (>=6) as a peer dependency to match the version range used by @lowdefy/blocks-antd.

v5.2.0 Breaking risk

First‑class reusable modules, new rich‑text editor blocks (Tiptap), improved S3 upload handling and metadata encoding, holdValue request flag for stale data prevention, and breaking SQLite/MySQL driver upgrades.

Full changelog

Highlights

  • Modules: a first-class system for reusable config packages. Apps can now declare modules in lowdefy.yaml from GitHub repos (github:owner/repo[/path]@ref) or local paths (file:./path), with namespaced page/connection/endpoint IDs, validated vars, declared exports, plugin compatibility checks, secret allowlists, connection remapping, and cross-module dependencies — replacing the copy-paste-between-projects pattern with a declarative dependency. New _module.var / _module.pageId / _module.connectionId / _module.endpointId / _module.id operators, _ref: { module, component, vars } for reusing config fragments, picomatch glob patterns in auth page rules, and server route support for slashed page IDs (e.g. /team-users/users-list).

  • Rich-text editing out of the box: new @lowdefy/blocks-tiptap package. Two new default blocks — TiptapInput (bold/italic/strike, multi-color highlight, headings, lists, tables, links, bubble menu) and TiptapMentionInput (adds @-mention dropdown from static options or a request). Both emit { html, text, markdown, fileList, mentions? } and expose clear / setContent / focus methods. Configurable per-extension via properties.starterKit / image / table / link / highlight / mentions. Image drag/drop and paste are supported by wiring properties.s3PostPolicyRequestId to an S3 presigned POST request. Uses the open-source @tiptap/extension-file-handler — no TIPTAP_PRO_TOKEN or scoped-registry config needed.

  • Server-to-server API calls via CallApi / InternalApi. API endpoint routines can now call other endpoints in-process (no HTTP) with isolated steps / payload namespaces and a recursion cap of 10. New InternalApi endpoint type is blocked from HTTP access; client CallAPI actions targeting InternalApi get a build warning (error in production). CallApi routine steps are validated at build time (require properties.endpointId, reject connectionId).

  • _js operator now accepts pre-computed args. Use the { fn, args } form and resolve values with any Lowdefy operator (_state, _request, _user, nested _js) before the JS runs — values are injected as the args object inside the function body. Keeps JS focused on computation while operator lookups stay in YAML. The string form still works, and identical fn bodies still share a compiled function at build time.

  • holdValue flag on Request and CallAPI actions. UI bound to _request: <id> or _api: <endpointId> retains the previous response while a refetch is loading (and on error), instead of flashing to null. The error is still observable via _request_details / _api.

  • ControlledList gains onAdd / onRemove events. Both fire after the list mutation with { index, item }onRemove captures the row value before deletion, so handlers can reference deleted data (e.g. _event: item._id to delete from a backend). The remove icon now defaults to var(--ant-color-error) at var(--ant-font-size-lg) with hover/active states, and list-type blocks now receive a value prop with the current list data.

  • S3 upload blocks: new onBeforeUpload event and antd v6 fixes. S3UploadButton, S3UploadDragger, and S3UploadPhoto fire onBeforeUpload before the upload starts — throw to cancel (validation, confirmation prompts, etc.). S3UploadDragger properties.height works again on antd v6 and accepts CSS lengths ("50vh", "300px"); all three blocks expose antd v6 semantic slots (trigger, list, item) through Lowdefy's classNames / styles API and are wrapped in withTheme('Upload', …) for per-instance Upload token overrides. S3Download adds showRemoveIcon and an onRemove event. Migration: styles previously targeting style: { .element: { … } } on the inner drag div should move to style: { .trigger: { … } }.

  • AwsS3PresignedPostPolicy auto URL-encodes x-amz-meta-* fields. S3 user-metadata values containing names, URLs, or non-ASCII characters no longer need to be wrapped with _uri.encode per request — the connection encodes them before signing. Note: readers of the metadata (e.g. Lambda S3-event triggers) should decodeURIComponent x-amz-meta-* values before use.

  • SQL drivers consolidated onto actively-maintained packages. @lowdefy/connection-knex upgraded knex 2.5.1 → 3.2.9, replaced sqlite3 with better-sqlite3 12.9.0, replaced mysql with mysql2 3.22.3, and replaced mssql with the tedious driver knex actually loads. Migration: apps using client: sqlite3 must switch to client: better-sqlite3 (or the client: sqlite alias); apps using client: mysql must switch to client: mysql2 — both throw a ConfigError with a migration message. client: mssql is unchanged. better-sqlite3 is allowlisted in pnpm.onlyBuiltDependencies so its native binding builds correctly under pnpm 10.

  • Build validation: schema errors are now warnings, with focused per-step validations. AJV schema mismatches no longer block the build — they emit warnings, and dedicated validations in validateBlock, buildConnections, buildEvents, etc. produce errors with full context (page, block, event names) instead of generic schema messages. New focused validation for connections and menu items.

  • Client rendering fixes. Container, InputContainer, and List no longer create empty content[slotKey] functions — blocks using the content.X && content.X() pattern correctly render nothing (no wrapping Area) when a slot is empty. Link variants (backLink, newOriginLink, sameOriginLink, noLink) now forward the style prop, fixing inline color: 'inherit' on header notification/profile/dark-mode rows, the disabled-anchor color, and per-link style: config in menu items.

  • Logger crash fix on literal null lines. JSON.parse can return null for "null" input — non-object parsed values are now treated as plain text instead of crashing the CLI log handler.

Changes

  • @lowdefy/api: ### Minor Changes

  • 73fa2b9: feat: Internal API endpoint calls

    Endpoint-to-Endpoint Calls (@lowdefy/api)

    • API endpoint routines can call other endpoints server-side via CallApi steps, without HTTP
    • Each called endpoint runs in an isolated context with its own steps and payload namespaces
    • Recursive endpoint call depth is capped at 10 to prevent infinite loops
    • InternalApi endpoints are blocked from HTTP access — they return the same response as a missing endpoint

    Build Support (@lowdefy/build)

    • CallApi routine steps validated at build time: require properties.endpointId, reject connectionId
    • InternalApi endpoint type accepted alongside Api
    • Client-side CallAPI actions targeting InternalApi endpoints produce a build warning (error in production)

    Operator Parser (@lowdefy/operators)

    • ServerParser.parse() accepts steps and payload per call for routine context isolation

Patch Changes

  • @lowdefy/build: ### Minor Changes

  • 73fa2b9: feat: Internal API endpoint calls

    Endpoint-to-Endpoint Calls (@lowdefy/api)

    • API endpoint routines can call other endpoints server-side via CallApi steps, without HTTP
    • Each called endpoint runs in an isolated context with its own steps and payload namespaces
    • Recursive endpoint call depth is capped at 10 to prevent infinite loops
    • InternalApi endpoints are blocked from HTTP access — they return the same response as a missing endpoint

    Build Support (@lowdefy/build)

    • CallApi routine steps validated at build time: require properties.endpointId, reject connectionId
    • InternalApi endpoint type accepted alongside Api
    • Client-side CallAPI actions targeting InternalApi endpoints produce a build warning (error in production)

    Operator Parser (@lowdefy/operators)

    • ServerParser.parse() accepts steps and payload per call for routine context isolation
  • 69a59c0: feat(_js): Pass pre-computed values into _js via an args object.

    The _js operator now accepts an object form { fn, args } alongside the existing string form. Values in args are resolved by the parser — using any Lowdefy operator (_state, _request, _user, nested _js, etc.) — before the JavaScript function runs, and are injected as the args object inside the function body.

    _js:
      fn: |
        const { products, target } = args;
        return products
          .filter((p) => p.category === target)
          .reduce((a, p) => a + p.price, 0);
      args:
        products:
          _request: get_products.data.products
        target: smartphones
    

    This lets you precompute or normalize values in YAML and keep the JavaScript body focused on computation, rather than mixing operator lookups into the function. The string form continues to work unchanged, and identical fn bodies still share a single compiled function at build time — only args varies per call.

  • 0f38c9f: feat: First-class module system for reusable config packages

    Modules are reusable bundles of Lowdefy config — pages, connections, API endpoints, menus, and exposed components — hosted in GitHub repositories or local directories. Apps install modules in lowdefy.yaml and configure them through vars, replacing the copy-paste-between-projects pattern with a declarative dependency.

    Module entries (@lowdefy/build)

    • Apps declare entries in the modules array of lowdefy.yaml with id, source, and optional vars, connections, and dependencies.
    • The entry id namespaces the module's content and forms the URL prefix for its pages (e.g. /team-users/users-list).
    • Multi-instance: the same module source can be installed multiple times under different entry IDs, each with its own vars and namespace.
    • GitHub sources (github:owner/repo[/path]@ref) are fetched as tarballs and locally cached. Private repos use GITHUB_TOKEN, the gh CLI, or git credential helpers.
    • Local sources (file:./relative/path) resolve relative to the project root.

    Module manifest (module.lowdefy.yaml)

    • Declares the module's interface: name, description, vars, connections, pages, api, components, menus, dependencies, exports, plugins, and secrets.
    • vars declarations validate consumer values with type, required, default, and description. Consumer values override manifest defaults; omitted values fall back to the declared default.
    • exports declares the module's public interface — the IDs other modules and apps may reference. The build validates cross-module references against exports.
    • plugins declarations are validated against the app's installed plugins with semver compatibility checks.
    • secrets is an allowlist of secrets the module may access; undeclared _secret references fail the build. Remapped connections skip the module's secret references for that connection.

    Module operators

    • _module.var — read manifest-validated vars, including consumer overrides and declared defaults.
    • _module.pageId, _module.connectionId, _module.endpointId — produce scoped IDs from a module-author's unscoped ID.
    • _module.id — the entry ID of the current module.

    Auto-scoped IDs

    Page, connection, API endpoint, and menu item IDs are auto-prefixed with the entry ID. Block and request IDs inherit page scope and are not rewritten.

    Consuming module resources

    • Pages and APIs are auto-included and auto-scoped — they appear in the app under the entry-ID prefix.
    • Components are reusable config fragments included with _ref: { module, component, vars }. They can export any config — UI blocks, enum maps, config templates, schema fragments — and accept vars at the call site.
    • Menus are included with _ref: { module, menu }, typically wrapped in a MenuGroup.

    Connection remapping

    Apps can redirect a module connection to an existing app connection via the entry's connections map. The module's connection definition and its declared secrets are skipped — the app connection handles them.

    Cross-module dependencies

    Modules can reference each other's pages, components, menus, connections, and APIs via abstract dependencies declared in module.lowdefy.yaml.

    • Auto-wiring: when a module entry's id matches a declared dependency name, the build wires it automatically.
    • Explicit wiring: the entry's dependencies map overrides auto-wiring and supports multi-instance topologies where each instance points at a different partner.
    • The build validates every wiring, detects dependency cycles, and reports unmapped or undeclared dependencies with remediation hints.

    Auth page rules

    Picomatch glob patterns in auth page rules (e.g. team-users/*) for wildcard module page matching.

    Slashed page IDs (@lowdefy/server, @lowdefy/server-dev)

    Server routes support module page IDs containing / (e.g. /team-users/users-list).

Patch Changes

  • 762755c: feat(blocks-tiptap): Add new default block package with TiptapInput and TiptapMentionInput rich-text editors.

    @lowdefy/blocks-tiptap ships two rich-text editor blocks built on TipTap:

    • TiptapInput — standard rich-text editor with bold/italic/strike-through, multi-color highlight, headings, lists, tables, links, and a bubble menu.
    • TiptapMentionInput — everything TiptapInput does, plus an @-mention dropdown populated from a static options list or a Lowdefy request. Resolved mentions are returned on the block value as mentions: [...].

    Both blocks emit an object value shaped { html, text, markdown, fileList, mentions? } and register clear, setContent, and focus methods.

    Configurable extensions — defaults preserve the bundled editor; override any of these to trim the editor down or tune it:

    • properties.starterKit — object forwarded to TipTap StarterKit, e.g. { heading: false, codeBlock: false }.
    • properties.image{ enabled, maxWidth, zoom }
    • properties.table{ enabled, resizable }
    • properties.link{ enabled, autolink, linkOnPaste, openOnClick, defaultProtocol }
    • properties.highlight{ enabled, multicolor }
    • properties.mentions.char / properties.mentions.allowSpaces — change the trigger char (e.g. # for hashtags) or disable spaces inside a mention query (TiptapMentionInput only).

    Image drag/drop and paste are supported by pointing properties.s3PostPolicyRequestId at a request that returns an S3 presigned POST policy (e.g. AwsS3PresignedPostPolicy). The file handler is optional — omit the request id to disable uploads entirely.

    The blocks are registered in the default types map and are available out of the box on @lowdefy/server-dev. No private-registry tokens are required: the blocks use the open-source @tiptap/extension-file-handler instead of @tiptap-pro/extension-file-handler, so projects that migrated from a custom TipTap plugin can drop their TIPTAP_PRO_TOKEN environment variable and .npmrc scoped-registry config.

  • 72b6159: fix(build): Replace schema validation errors with warnings and add focused validations.

    AJV schema validation now emits warnings instead of blocking the build. Focused validations in each build step (validateBlock, buildConnections, buildEvents, etc.) provide better error messages with full context — page, block, and event names — instead of generic schema messages. Added focused validation for connections and menu items that previously relied on schema checks alone.

  • @lowdefy/client: ### Patch Changes

  • 01e249b: feat(blocks-antd): ControlledList now fires onAdd / onRemove events and defaults the remove icon to the antd error color at a standard size.

    Events. Both events fire after the list mutation completes. The event payload is { index, item }:

    • onAddindex is where the new row was inserted (0 for addToFront: true, else list.length). item is the newly added value (typically undefined for an empty row).
    • onRemoveindex is the removed row's position. item is the row value captured before removal, so handlers can reference the deleted data (e.g., _event: item._id to delete from a backend).
    - id: tags
      type: ControlledList
      events:
        onRemove:
          - id: notify
            type: DisplayMessage
            params:
              content:
                _string.concat: ['Removed at index ', { _event: index }]
      blocks:
        - id: tags.$.label
          type: TextInput
    

    Remove icon styling. The remove icon now defaults to var(--ant-color-error) at var(--ant-font-size-lg), with --ant-color-error-hover / --ant-color-error-active on hover/press — no more hardcoded hex colors, and the size no longer swings with properties.size. Override via class.removeIcon / style.removeIcon (both slots target the icon wrapper). Existing configs that hardcoded color: '#ff4d4f' on removeItemIcon can drop it — the default is already danger.

    @lowdefy/client also now passes the list's current state value to list-type block components via a value prop, so any list block can read its own array data.

  • a4ecee5: fix(client): Skip rendering content slots that have no blocks.

    Container, InputContainer, and List no longer create a content[slotKey] function when the slot's blocks array is empty. Blocks that use the content.X && content.X() pattern (for optional header, footer, extra, etc.) now correctly render nothing — including no wrapping Area element — when the user leaves the slot empty.

  • 6ec0dd4: fix(client): Forward style prop to all Link variants.

    createLinkComponent previously destructured every prop except style, so any <Link style={...}> passed by a block was silently dropped. Inline style overrides only worked via className + CSS. All four link variants (backLink, newOriginLink, sameOriginLink — both newTab and same-origin branches — and noLink) now thread style through to the rendered <a> (or <span> for noLink).

    Surfaces fixes in three places that were already passing style and silently broken: headerActions.js notifications/profile/dark-mode rows had color: 'inherit' that didn't reach the <a> (label rendered as antd link blue); Anchor.js disabled state set color: '#BEBEBE' that never applied; buildMenuItems.js per-link style: config was discarded.

  • @lowdefy/engine: ### Minor Changes

  • 1d18a13: feat(actions): holdValue flag on Request and CallAPI actions.

    Request and CallAPI actions now accept a holdValue: true flag that retains the previous response value while a new call is loading. UI bound to _request: <id> or _api: <endpointId> keeps showing the previous response instead of flashing to null during a refetch. The previous response is also retained if the new call errors — the error is still observable via _request_details / _api.

    - id: refresh_table
      type: Request
      params:
        requestId: my_table_request
        holdValue: true
    

    The Request action's object-form params now also support { requestId, holdValue } and { requestIds, holdValue } shapes alongside the existing { all } shape.

Patch Changes

  • d105b81: fix(engine): Preserve input values across visible toggles when set via SetState.

    When an input was inside a hidden container and a SetState action set its value (e.g. on onInit before the container becomes visible), the value was silently reset to the input's enforceType default (typically null/"") on the next SetState. The next SetState triggered RootSlots.reset(), which found the field missing from context.state (because Slots.updateState correctly deletes invisible blocks' state fields) and overwrote this.value with the type default.

    Block.reset() now skips the type-default fallback when the block was hidden in the previous eval cycle and has in-memory state to preserve — this.value for inputs, or existing subSlots for lists (which would otherwise be truncated by the rebuild loop reading an empty enforceType('array', null)). The next updateState republishes the value to context.state if the block becomes visible, or leaves the field absent if it stays hidden.

    This brings SetState-driven visibility toggles into parity with setValue-driven toggles, which already preserved the value via Block.evaluate. Lists with nested inputs also retain per-item values across hide/reveal cycles.

    Behaviour change: apps that relied on a hidden input being silently reset to its default by an unrelated SetState will now see the previously-set value preserved. To explicitly clear a value, use SetState({ myInput: null }). Invisible blocks continue to have no representation in context.state, the user-facing Reset action still produces enforceType defaults, and List sub-slot rebuilding on SetState({ list: [...] }) is unchanged.

  • @lowdefy/operators: ### Patch Changes

  • 73fa2b9: feat: Internal API endpoint calls

    Endpoint-to-Endpoint Calls (@lowdefy/api)

    • API endpoint routines can call other endpoints server-side via CallApi steps, without HTTP
    • Each called endpoint runs in an isolated context with its own steps and payload namespaces
    • Recursive endpoint call depth is capped at 10 to prevent infinite loops
    • InternalApi endpoints are blocked from HTTP access — they return the same response as a missing endpoint

    Build Support (@lowdefy/build)

    • CallApi routine steps validated at build time: require properties.endpointId, reject connectionId
    • InternalApi endpoint type accepted alongside Api
    • Client-side CallAPI actions targeting InternalApi endpoints produce a build warning (error in production)

    Operator Parser (@lowdefy/operators)

    • ServerParser.parse() accepts steps and payload per call for routine context isolation
  • 1e964c4: fix(operators): Preserve source location on build operator results.

    Build operator results (e.g. from _build.array.concat) now retain the source file and line number of the expression that produced them. Previously, operator evaluation replaced the expression object with a fresh result, losing the source location markers. This caused build errors inside operator-produced arrays (such as null blocks) to show the file path but no line number.

  • @lowdefy/actions-core: ### Minor Changes

  • 1d18a13: feat(actions): holdValue flag on Request and CallAPI actions.

    Request and CallAPI actions now accept a holdValue: true flag that retains the previous response value while a new call is loading. UI bound to _request: <id> or _api: <endpointId> keeps showing the previous response instead of flashing to null during a refetch. The previous response is also retained if the new call errors — the error is still observable via _request_details / _api.

    - id: refresh_table
      type: Request
      params:
        requestId: my_table_request
        holdValue: true
    

    The Request action's object-form params now also support { requestId, holdValue } and { requestIds, holdValue } shapes alongside the existing { all } shape.

Patch Changes

  • @lowdefy/[email protected]

  • @lowdefy/[email protected]

  • @lowdefy/blocks-aggrid: ### Patch Changes

  • 186a57d: fix(blocks-aggrid): Suppress cell focus by default and clip overflowing cell content.

    The ag-grid cell focus outline visually competed with built-in cell renderers (buttons, links, tags), so suppressCellFocus now defaults to true and can still be overridden per grid. The antd cell wrapper also clips overflowing flex children so long text and inline cell components no longer blow out the cell width.

  • @lowdefy/blocks-antd: ### Minor Changes

  • 01e249b: feat(blocks-antd): ControlledList now fires onAdd / onRemove events and defaults the remove icon to the antd error color at a standard size.

    Events. Both events fire after the list mutation completes. The event payload is { index, item }:

    • onAddindex is where the new row was inserted (0 for addToFront: true, else list.length). item is the newly added value (typically undefined for an empty row).
    • onRemoveindex is the removed row's position. item is the row value captured before removal, so handlers can reference the deleted data (e.g., _event: item._id to delete from a backend).
    - id: tags
      type: ControlledList
      events:
        onRemove:
          - id: notify
            type: DisplayMessage
            params:
              content:
                _string.concat: ['Removed at index ', { _event: index }]
      blocks:
        - id: tags.$.label
          type: TextInput
    

    Remove icon styling. The remove icon now defaults to var(--ant-color-error) at var(--ant-font-size-lg), with --ant-color-error-hover / --ant-color-error-active on hover/press — no more hardcoded hex colors, and the size no longer swings with properties.size. Override via class.removeIcon / style.removeIcon (both slots target the icon wrapper). Existing configs that hardcoded color: '#ff4d4f' on removeItemIcon can drop it — the default is already danger.

    @lowdefy/client also now passes the list's current state value to list-type block components via a value prop, so any list block can read its own array data.

Patch Changes

  • 6ec2cd9: fix(PageSidebarLayout): Pin the sider to the viewport so the bottom actions stay visible.

    The sider is now position: sticky with height: 100vh, so the menu, notifications, profile avatar, dark-mode toggle, and logo remain on screen as the page content scrolls. The sticky footer container fades from transparent to the container background so content passing behind it doesn't cut off abruptly.

  • fd1604f: feat(blocks-antd): DropdownButton now supports the standard Lowdefy event-shortcut schema (events.<eventName>.shortcut) for item shortcuts, alongside the existing item-level shortcut property.

    When a shortcut is configured via the event, the framework-level shortcut manager binds and fires it — consistent with Button. The shortcut badge renders next to the item label in both cases. If both are set on the same item, the event-level shortcut wins. The split-button's main action now also renders a badge when events.onClick.shortcut is configured.

    Event-level (preferred, matches Button):

    - id: actions
      type: DropdownButton
      properties:
        items:
          - title: Undo
            eventName: onUndo
      events:
        onUndo:
          shortcut: mod+z
          try:
            - id: undo
              type: ...
    

    Item-level (still supported):

    - id: actions
      type: DropdownButton
      properties:
        items:
          - title: Undo
            eventName: onUndo
            shortcut: mod+z
      events:
        onUndo:
          - id: undo
            type: ...
    
  • cea34ac: fix(PageSidebarLayout): Render notifications, profile, and dark-mode toggle as labeled, left-aligned rows when the sider is expanded.

    When the sider is open, the bottom actions now render as [icon] [label] rows that match the visual style of the menu items above (e.g. "Notifications", "Profile", "Light mode"). When the sider is collapsed, the actions remain as a centered icon stack. Two new optional schema fields — notifications.title (default Notifications) and profile.title (default Profile) — let consumers customise the expanded labels; consumers can also bind _user: name to profile.title to show the authenticated user's name.

    The profile dropdown's default trigger now depends on whether the sider is expanded: click when expanded (the labeled row invites click), hover when collapsed (original small-avatar behavior). Consumers passing profile.trigger explicitly are unaffected.

    No change to PageSiderMenu or PageHeaderMenu — their header-bar rendering still uses the icon-only layout and hover trigger.

  • @lowdefy/blocks-loaders: ### Patch Changes

  • c91003d: refactor(blocks-loaders): Modernize skeleton shimmer animation.

    The skeleton placeholder now uses a compositor-accelerated transform: translateX shimmer on a ::after pseudo-element, replacing the older left-animated ::before gradient. The shimmer is softer (three-stop gradient over secondary/quaternary fills), the base element uses isolation: isolate to scope its stacking context, and prefers-reduced-motion disables the animation for accessibility.

    SkeletonInput and SkeletonParagraph also switch to a flex-column layout with gap spacing: tighter rhythm between rows, distinct border radii for the label vs input, and a slightly wider trailing line (60%) in paragraphs for a more readable look. Purely visual — no API changes.

  • @lowdefy/blocks-tiptap: ### Minor Changes

  • 762755c: feat(blocks-tiptap): Add new default block package with TiptapInput and TiptapMentionInput rich-text editors.

    @lowdefy/blocks-tiptap ships two rich-text editor blocks built on TipTap:

    • TiptapInput — standard rich-text editor with bold/italic/strike-through, multi-color highlight, headings, lists, tables, links, and a bubble menu.
    • TiptapMentionInput — everything TiptapInput does, plus an @-mention dropdown populated from a static options list or a Lowdefy request. Resolved mentions are returned on the block value as mentions: [...].

    Both blocks emit an object value shaped { html, text, markdown, fileList, mentions? } and register clear, setContent, and focus methods.

    Configurable extensions — defaults preserve the bundled editor; override any of these to trim the editor down or tune it:

    • properties.starterKit — object forwarded to TipTap StarterKit, e.g. { heading: false, codeBlock: false }.
    • properties.image{ enabled, maxWidth, zoom }
    • properties.table{ enabled, resizable }
    • properties.link{ enabled, autolink, linkOnPaste, openOnClick, defaultProtocol }
    • properties.highlight{ enabled, multicolor }
    • properties.mentions.char / properties.mentions.allowSpaces — change the trigger char (e.g. # for hashtags) or disable spaces inside a mention query (TiptapMentionInput only).

    Image drag/drop and paste are supported by pointing properties.s3PostPolicyRequestId at a request that returns an S3 presigned POST policy (e.g. AwsS3PresignedPostPolicy). The file handler is optional — omit the request id to disable uploads entirely.

    The blocks are registered in the default types map and are available out of the box on @lowdefy/server-dev. No private-registry tokens are required: the blocks use the open-source @tiptap/extension-file-handler instead of @tiptap-pro/extension-file-handler, so projects that migrated from a custom TipTap plugin can drop their TIPTAP_PRO_TOKEN environment variable and .npmrc scoped-registry config.

Patch Changes

  • @lowdefy/connection-knex: ### Minor Changes

  • 596fddc: chore(connection-knex): update knex and SQL drivers; replace sqlite3 with better-sqlite3; replace mysql with mysql2.

    Bumped knex and its dialect drivers, and consolidated onto the actively-maintained drivers — replaced sqlite3 with better-sqlite3 and mysql with mysql2. Subsumes the prior [email protected] darwin-arm64 fix.

    @lowdefy/connection-knex dependency changes:

    • knex 2.5.13.2.9. Knex 3.x drops Node < 16; Lowdefy already requires Node 18+. The knex(config), .raw(), and dynamic query-builder API surface used by KnexRaw / KnexBuilder is unchanged.
    • pg 8.11.38.20.0.
    • Removed mssql. Knex's mssql dialect actually requires tedious (not the mssql package), and Lowdefy never imported mssql directly — it was only ever a vehicle for pulling tedious into the install tree. client: mssql in user YAML is unchanged: the knex client name stays the same, only the underlying npm package shipped with connection-knex changes.
    • Added tedious 19.2.1 as the SQL Server driver — the package knex actually loads when client: mssql is used.
    • Removed sqlite3. The driver is in maintenance-only mode upstream (the v6 release marked the repo unmaintained).
    • Added better-sqlite3 12.9.0 as the SQLite driver. Selectable as client: better-sqlite3 (or client: sqlite, which is now an alias of better-sqlite3 — see runtime client handling below).
    • Removed mysql. Unmaintained upstream since 2020.
    • Added mysql2 3.22.3 as the MySQL / MariaDB driver. Selectable as client: mysql2 in connection YAML.

    Runtime client handling (in createKnex):

    • client: sqlite is silently remapped to client: better-sqlite3. sqlite was historically a knex-level alias of sqlite3; this preserves the YAML alias while the underlying driver changes.
    • client: sqlite3 now throws a ConfigError with a migration message: Knex connection "client: sqlite3" is no longer supported. Use "client: better-sqlite3" or "client: sqlite" instead. Existing apps using client: sqlite3 need to update their connection YAML.
    • client: mysql now throws a ConfigError with a migration message: Knex connection "client: mysql" is no longer supported. Use "client: mysql2" instead. Existing apps using client: mysql need to update their connection YAML. mysql is not silently remapped because knex treats mysql and mysql2 as separate dialects with subtly different SQL formatters, not aliases — the migration is a deliberate user choice.

    pnpm.onlyBuiltDependencies allowlist for better-sqlite3:

    better-sqlite3 runs a native-binding install script (prebuild-install with a node-gyp rebuild fallback). pnpm 10 silently suppresses postinstall scripts for unapproved packages, which leaves the binding unbuilt and crashes KnexRaw / KnexBuilder at runtime.

    • Added better-sqlite3 to the allowlist on @lowdefy/server, @lowdefy/server-dev, and @lowdefy/server-e2e. These are the install roots in the CLI fetch flow under .lowdefy/{dev,build}/, where pnpm honors the per-package pnpm.onlyBuiltDependencies field.
    • Also added the same allowlist to the monorepo root package.json. The per-package field is ignored at workspace-root install (pnpm 10 only honors it on the install root), so contributors running pnpm install at the repo root would otherwise have to pnpm rebuild better-sqlite3 manually.

Patch Changes

  • @lowdefy/[email protected]

  • @lowdefy/[email protected]

  • @lowdefy/operators-js: ### Minor Changes

  • 69a59c0: feat(_js): Pass pre-computed values into _js via an args object.

    The _js operator now accepts an object form { fn, args } alongside the existing string form. Values in args are resolved by the parser — using any Lowdefy operator (_state, _request, _user, nested _js, etc.) — before the JavaScript function runs, and are injected as the args object inside the function body.

    _js:
      fn: |
        const { products, target } = args;
        return products
          .filter((p) => p.category === target)
          .reduce((a, p) => a + p.price, 0);
      args:
        products:
          _request: get_products.data.products
        target: smartphones
    

    This lets you precompute or normalize values in YAML and keep the JavaScript body focused on computation, rather than mixing operator lookups into the function. The string form continues to work unchanged, and identical fn bodies still share a single compiled function at build time — only args varies per call.

  • 0d44433: feat(_string): Add _string.format for template-style string interpolation.

    _string.format substitutes placeholders in a template string with values, accepting either a positional array form or a named object form. null/undefined values render as empty strings, which often makes _if_none unnecessary.

    # Positional placeholders
    _string.format:
      - 'Updates ({0})'
      - _request: get_counts.0.update
    
    # Named placeholders
    _string.format:
      template: 'Updates ({count}) since {date}'
      on:
        count:
          _request: get_counts.0.update
        date:
          _date.format:
            - YYYY-MM-DD
            - _state: lastSync
    

    Use {{ / }} to include literal braces. Prefer _string.format over _string.concat for label-style interpolation, and use _nunjucks when you need conditionals, loops, or filters.

Patch Changes

  • 1d18a13: feat(actions): holdValue flag on Request and CallAPI actions.

    Request and CallAPI actions now accept a holdValue: true flag that retains the previous response value while a new call is loading. UI bound to _request: <id> or _api: <endpointId> keeps showing the previous response instead of flashing to null during a refetch. The previous response is also retained if the new call errors — the error is still observable via _request_details / _api.

    - id: refresh_table
      type: Request
      params:
        requestId: my_table_request
        holdValue: true
    

    The Request action's object-form params now also support { requestId, holdValue } and { requestIds, holdValue } shapes alongside the existing { all } shape.

  • @lowdefy/plugin-aws: ### Minor Changes

  • 235e219: feat(plugin-aws): Add onBeforeUpload event to S3 upload blocks.

    S3UploadButton, S3UploadDragger, and S3UploadPhoto now fire an onBeforeUpload event before the upload starts. If any action throws, the upload is cancelled. This allows validation, confirmation prompts, or other logic to run before files are sent to S3.

Patch Changes

  • a52db1c: feat(plugin-aws): Auto URL-encode x-amz-meta-* fields on AwsS3PresignedPostPolicy.

    AwsS3PresignedPostPolicy now URL-encodes any field whose name starts with x-amz-meta- (case-insensitive) before signing the policy. S3 user metadata must be ASCII, so values containing names, URLs, or other non-ASCII characters previously had to be wrapped with _uri.encode in every request config.

    With this change, upload policies can pass values through directly:

    fields:
      x-amz-meta-uploaded-by-name:
        _user: profile.name
      x-amz-meta-uploaded-by-url:
        _payload: url
    

    Other protocol fields (acl, Content-Type, success_action_redirect, etc.) continue to pass through literally. Readers of the metadata (e.g., Lambda triggers that ingest S3 events) should URL-decode x-amz-meta-* values via decodeURIComponent before use.

  • 825f86c: fix(plugin-aws): S3 upload blocks — working height on S3UploadDragger in antd v6, antd semantic-slot passthrough across all three upload blocks, cleaner classNames/styles wiring.

    After the antd v6 upgrade the Upload wrapper changed from a <div> to an inline <span>, which caused the antd Dragger height prop to resize only the inner drag box — the wrapping element and the clickable ant-upload-btn (which uses height: 100%) collapsed, so the drag area looked unchanged.

    This release:

    • Makes properties.height actually resize the full drag surface. Height is applied to the block's outer wrapper, and the antd Upload wrapper is forced to display: block; height: 100% so nested height: 100% rules resolve correctly.
    • Defaults height to the antd controlHeight theme token (matches the default button height and follows the compact algorithm).
    • Accepts height as a number or a string so CSS lengths like "50vh" / "300px" are supported.
    • Exposes the antd v6 Upload semantic slots (trigger, list, item) through Lowdefy's classNames / styles API, so file-list rows and the drop trigger can be styled from YAML (style: { .trigger: { … }, .list: { … }, .item: { … } }).
    • Moves classNames.element / styles.element onto the block's outer wrapper (matching the convention used in blocks-antd/Search), merges marker classes via cn(), and adds a lf-s3-upload-dragger-hint marker to the hint node for future scoped CSS.
    • Fixes precedence: style.element.height now overrides properties.height again, matching the original schema docs.

    S3UploadButton and S3UploadPhoto receive the same treatment: classNames.element / styles.element move to an outer block wrapper carrying a marker class (lf-s3-upload-button, lf-s3-upload-photo), and the antd v6 semantic slots trigger / list / item are piped through as Lowdefy slots. S3UploadPhoto also keeps .icon and .title slots for styling the content of the upload trigger card. The avatar-uploader hardcoded class on S3UploadPhoto (which had no CSS attached) was removed in favour of the marker-class pattern.

    S3UploadDragger, S3UploadButton, and S3UploadPhoto are now wrapped in withTheme('Upload', …) (previously only S3Download was). Each block's meta.js exposes a theme property whose object is forwarded into a scoped <ConfigProvider theme={{ components: { Upload: theme } }}>, giving per-instance overrides of antd Upload design tokens (actionsColor, pictureCardSize, controlItemBgHover, colorIcon, fontSize, borderRadiusSM) — the tokens that semantic classNames / styles slots cannot reach. The shared schema lives at packages/plugins/plugins/plugin-aws/src/schemas/uploadTheme.js and is imported by all four upload/download block metas.

    S3Download gains a showRemoveIcon property (default false) and an onRemove event. When the remove icon is clicked, the block fires onRemove with the clicked file and returns false to antd — the controlled fileList stays authoritative, and the action handler decides whether to update state (e.g. via SetState).

    Migration note: Apps that relied on style: { .element: { … } } styling the inner .ant-upload-drag / .ant-upload-select div (e.g. custom hover interactions coupled to that selector) should move those declarations to style: { .trigger: { … } }. All visual styling cases in the gallery (background, border, shadow, padding) render identically on the outer wrapper, so typical apps need no changes.

  • @lowdefy/server: ### Minor Changes

  • 0f38c9f: feat: First-class module system for reusable config packages

    Modules are reusable bundles of Lowdefy config — pages, connections, API endpoints, menus, and exposed components — hosted in GitHub repositories or local directories. Apps install modules in lowdefy.yaml and configure them through vars, replacing the copy-paste-between-projects pattern with a declarative dependency.

    Module entries (@lowdefy/build)

    • Apps declare entries in the modules array of lowdefy.yaml with id, source, and optional vars, connections, and dependencies.
    • The entry id namespaces the module's content and forms the URL prefix for its pages (e.g. /team-users/users-list).
    • Multi-instance: the same module source can be installed multiple times under different entry IDs, each with its own vars and namespace.
    • GitHub sources (github:owner/repo[/path]@ref) are fetched as tarballs and locally cached. Private repos use GITHUB_TOKEN, the gh CLI, or git credential helpers.
    • Local sources (file:./relative/path) resolve relative to the project root.

    Module manifest (module.lowdefy.yaml)

    • Declares the module's interface: name, description, vars, connections, pages, api, components, menus, dependencies, exports, plugins, and secrets.
    • vars declarations validate consumer values with type, required, default, and description. Consumer values override manifest defaults; omitted values fall back to the declared default.
    • exports declares the module's public interface — the IDs other modules and apps may reference. The build validates cross-module references against exports.
    • plugins declarations are validated against the app's installed plugins with semver compatibility checks.
    • secrets is an allowlist of secrets the module may access; undeclared _secret references fail the build. Remapped connections skip the module's secret references for that connection.

    Module operators

    • _module.var — read manifest-validated vars, including consumer overrides and declared defaults.
    • _module.pageId, _module.connectionId, _module.endpointId — produce scoped IDs from a module-author's unscoped ID.
    • _module.id — the entry ID of the current module.

    Auto-scoped IDs

    Page, connection, API endpoint, and menu item IDs are auto-prefixed with the entry ID. Block and request IDs inherit page scope and are not rewritten.

    Consuming module resources

    • Pages and APIs are auto-included and auto-scoped — they appear in the app under the entry-ID prefix.
    • Components are reusable config fragments included with _ref: { module, component, vars }. They can export any config — UI blocks, enum maps, config templates, schema fragments — and accept vars at the call site.
    • Menus are included with _ref: { module, menu }, typically wrapped in a MenuGroup.

    Connection remapping

    Apps can redirect a module connection to an existing app connection via the entry's connections map. The module's connection definition and its declared secrets are skipped — the app connection handles them.

    Cross-module dependencies

    Modules can reference each other's pages, components, menus, connections, and APIs via abstract dependencies declared in module.lowdefy.yaml.

    • Auto-wiring: when a module entry's id matches a declared dependency name, the build wires it automatically.
    • Explicit wiring: the entry's dependencies map overrides auto-wiring and supports multi-instance topologies where each instance points at a different partner.
    • The build validates every wiring, detects dependency cycles, and reports unmapped or undeclared dependencies with remediation hints.

    Auth page rules

    Picomatch glob patterns in auth page rules (e.g. team-users/*) for wildcard module page matching.

    Slashed page IDs (@lowdefy/server, @lowdefy/server-dev)

    Server routes support module page IDs containing / (e.g. /team-users/users-list).

Patch Changes

  • 596fddc: chore(connection-knex): update knex and SQL drivers; replace sqlite3 with better-sqlite3; replace mysql with mysql2.

    Bumped knex and its dialect drivers, and consolidated onto the actively-maintained drivers — replaced sqlite3 with better-sqlite3 and mysql with mysql2. Subsumes the prior [email protected] darwin-arm64 fix.

    @lowdefy/connection-knex dependency changes:

    • knex 2.5.13.2.9. Knex 3.x drops Node < 16; Lowdefy already requires Node 18+. The knex(config), .raw(), and dynamic query-builder API surface used by KnexRaw / KnexBuilder is unchanged.
    • pg 8.11.38.20.0.
    • Removed mssql. Knex's mssql dialect actually requires tedious (not the mssql package), and Lowdefy never imported mssql directly — it was only ever a vehicle for pulling tedious into the install tree. client: mssql in user YAML is unchanged: the knex client name stays the same, only the underlying npm package shipped with connection-knex changes.
    • Added tedious 19.2.1 as the SQL Server driver — the package knex actually loads when client: mssql is used.
    • Removed sqlite3. The driver is in maintenance-only mode upstream (the v6 release marked the repo unmaintained).
    • Added better-sqlite3 12.9.0 as the SQLite driver. Selectable as client: better-sqlite3 (or client: sqlite, which is now an alias of better-sqlite3 — see runtime client handling below).
    • Removed mysql. Unmaintained upstream since 2020.
    • Added mysql2 3.22.3 as the MySQL / MariaDB driver. Selectable as client: mysql2 in connection YAML.

    Runtime client handling (in createKnex):

    • client: sqlite is silently remapped to client: better-sqlite3. sqlite was historically a knex-level alias of sqlite3; this preserves the YAML alias while the underlying driver changes.
    • client: sqlite3 now throws a ConfigError with a migration message: Knex connection "client: sqlite3" is no longer supported. Use "client: better-sqlite3" or "client: sqlite" instead. Existing apps using client: sqlite3 need to update their connection YAML.
    • client: mysql now throws a ConfigError with a migration message: Knex connection "client: mysql" is no longer supported. Use "client: mysql2" instead. Existing apps using client: mysql need to update their connection YAML. mysql is not silently remapped because knex treats mysql and mysql2 as separate dialects with subtly different SQL formatters, not aliases — the migration is a deliberate user choice.

    pnpm.onlyBuiltDependencies allowlist for better-sqlite3:

    better-sqlite3 runs a native-binding install script (prebuild-install with a node-gyp rebuild fallback). pnpm 10 silently suppresses postinstall scripts for unapproved packages, which leaves the binding unbuilt and crashes KnexRaw / KnexBuilder at runtime.

    • Added better-sqlite3 to the allowlist on @lowdefy/server, @lowdefy/server-dev, and @lowdefy/server-e2e. These are the install roots in the CLI fetch flow under .lowdefy/{dev,build}/, where pnpm honors the per-package pnpm.onlyBuiltDependencies field.
    • Also added the same allowlist to the monorepo root package.json. The per-package field is ignored at workspace-root install (pnpm 10 only honors it on the install root), so contributors running pnpm install at the repo root would otherwise have to pnpm rebuild better-sqlite3 manually.
  • @lowdefy/server-dev: ### Minor Changes

  • 0f38c9f: feat: First-class module system for reusable config packages

    Modules are reusable bundles of Lowdefy config — pages, connections, API endpoints, menus, and exposed components — hosted in GitHub repositories or local directories. Apps install modules in lowdefy.yaml and configure them through vars, replacing the copy-paste-between-projects pattern with a declarative dependency.

    Module entries (@lowdefy/build)

    • Apps declare entries in the modules array of lowdefy.yaml with id, source, and optional vars, connections, and dependencies.
    • The entry id namespaces the module's content and forms the URL prefix for its pages (e.g. /team-users/users-list).
    • Multi-instance: the same module source can be installed multiple times under different entry IDs, each with its own vars and namespace.
    • GitHub sources (github:owner/repo[/path]@ref) are fetched as tarballs and locally cached. Private repos use GITHUB_TOKEN, the gh CLI, or git credential helpers.
    • Local sources (file:./relative/path) resolve relative to the project root.

    Module manifest (module.lowdefy.yaml)

    • Declares the module's interface: name, description, vars, connections, pages, api, components, menus, dependencies, exports, plugins, and secrets.
    • vars declarations validate consumer values with type, required, default, and description. Consumer values override manifest defaults; omitted values fall back to the declared default.
    • exports declares the module's public interface — the IDs other modules and apps may reference. The build validates cross-module references against exports.
    • plugins declarations are validated against the app's installed plugins with semver compatibility checks.
    • secrets is an allowlist of secrets the module may access; undeclared _secret references fail the build. Remapped connections skip the module's secret references for that connection.

    Module operators

    • _module.var — read manifest-validated vars, including consumer overrides and declared defaults.
    • _module.pageId, _module.connectionId, _module.endpointId — produce scoped IDs from a module-author's unscoped ID.
    • _module.id — the entry ID of the current module.

    Auto-scoped IDs

    Page, connection, API endpoint, and menu item IDs are auto-prefixed with the entry ID. Block and request IDs inherit page scope and are not rewritten.

    Consuming module resources

    • Pages and APIs are auto-included and auto-scoped — they appear in the app under the entry-ID prefix.
    • Components are reusable config fragments included with _ref: { module, component, vars }. They can export any config — UI blocks, enum maps, config templates, schema fragments — and accept vars at the call site.
    • Menus are included with _ref: { module, menu }, typically wrapped in a MenuGroup.

    Connection remapping

    Apps can redirect a module connection to an existing app connection via the entry's connections map. The module's connection definition and its declared secrets are skipped — the app connection handles them.

    Cross-module dependencies

    Modules can reference each other's pages, components, menus, connections, and APIs via abstract dependencies declared in module.lowdefy.yaml.

    • Auto-wiring: when a module entry's id matches a declared dependency name, the build wires it automatically.
    • Explicit wiring: the entry's dependencies map overrides auto-wiring and supports multi-instance topologies where each instance points at a different partner.
    • The build validates every wiring, detects dependency cycles, and reports unmapped or undeclared dependencies with remediation hints.

    Auth page rules

    Picomatch glob patterns in auth page rules (e.g. team-users/*) for wildcard module page matching.

    Slashed page IDs (@lowdefy/server, @lowdefy/server-dev)

    Server routes support module page IDs containing / (e.g. /team-users/users-list).

Patch Changes

  • 596fddc: chore(connection-knex): update knex and SQL drivers; replace sqlite3 with better-sqlite3; replace mysql with mysql2.

    Bumped knex and its dialect drivers, and consolidated onto the actively-maintained drivers — replaced sqlite3 with better-sqlite3 and mysql with mysql2. Subsumes the prior [email protected] darwin-arm64 fix.

    @lowdefy/connection-knex dependency changes:

    • knex 2.5.13.2.9. Knex 3.x drops Node < 16; Lowdefy already requires Node 18+. The knex(config), .raw(), and dynamic query-builder API surface used by KnexRaw / KnexBuilder is unchanged.
    • pg 8.11.38.20.0.
    • Removed mssql. Knex's mssql dialect actually requires tedious (not the mssql package), and Lowdefy never imported mssql directly — it was only ever a vehicle for pulling tedious into the install tree. client: mssql in user YAML is unchanged: the knex client name stays the same, only the underlying npm package shipped with connection-knex changes.
    • Added tedious 19.2.1 as the SQL Server driver — the package knex actually loads when client: mssql is used.
    • Removed sqlite3. The driver is in maintenance-only mode upstream (the v6 release marked the repo unmaintained).
    • Added better-sqlite3 12.9.0 as the SQLite driver. Selectable as client: better-sqlite3 (or client: sqlite, which is now an alias of better-sqlite3 — see runtime client handling below).
    • Removed mysql. Unmaintained upstream since 2020.
    • Added mysql2 3.22.3 as the MySQL / MariaDB driver. Selectable as client: mysql2 in connection YAML.

    Runtime client handling (in createKnex):

    • client: sqlite is silently remapped to client: better-sqlite3. sqlite was historically a knex-level alias of sqlite3; this preserves the YAML alias while the underlying driver changes.
    • client: sqlite3 now throws a ConfigError with a migration message: Knex connection "client: sqlite3" is no longer supported. Use "client: better-sqlite3" or "client: sqlite" instead. Existing apps using client: sqlite3 need to update their connection YAML.
    • client: mysql now throws a ConfigError with a migration message: Knex connection "client: mysql" is no longer supported. Use "client: mysql2" instead. Existing apps using client: mysql need to update their connection YAML. mysql is not silently remapped because knex treats mysql and mysql2 as separate dialects with subtly different SQL formatters, not aliases — the migration is a deliberate user choice.

    pnpm.onlyBuiltDependencies allowlist for better-sqlite3:

    better-sqlite3 runs a native-binding install script (prebuild-install with a node-gyp rebuild fallback). pnpm 10 silently suppresses postinstall scripts for unapproved packages, which leaves the binding unbuilt and crashes KnexRaw / KnexBuilder at runtime.

    • Added better-sqlite3 to the allowlist on @lowdefy/server, @lowdefy/server-dev, and @lowdefy/server-e2e. These are the install roots in the CLI fetch flow under .lowdefy/{dev,build}/, where pnpm honors the per-package pnpm.onlyBuiltDependencies field.
    • Also added the same allowlist to the monorepo root package.json. The per-package field is ignored at workspace-root install (pnpm 10 only honors it on the install root), so contributors running pnpm install at the repo root would otherwise have to pnpm rebuild better-sqlite3 manually.
  • 762755c: feat(blocks-tiptap): Add new default block package with TiptapInput and TiptapMentionInput rich-text editors.

    @lowdefy/blocks-tiptap ships two rich-text editor blocks built on TipTap:

    • TiptapInput — standard rich-text editor with bold/italic/strike-through, multi-color highlight, headings, lists, tables, links, and a bubble menu.
    • TiptapMentionInput — everything TiptapInput does, plus an @-mention dropdown populated from a static options list or a Lowdefy request. Resolved mentions are returned on the block value as mentions: [...].

    Both blocks emit an object value shaped { html, text, markdown, fileList, mentions? } and register clear, setContent, and focus methods.

    Configurable extensions — defaults preserve the bundled editor; override any of these to trim the editor down or tune it:

    • properties.starterKit — object forwarded to TipTap StarterKit, e.g. { heading: false, codeBlock: false }.
    • properties.image{ enabled, maxWidth, zoom }
    • properties.table{ enabled, resizable }
    • properties.link{ enabled, autolink, linkOnPaste, openOnClick, defaultProtocol }
    • properties.highlight{ enabled, multicolor }
    • properties.mentions.char / properties.mentions.allowSpaces — change the trigger char (e.g. # for hashtags) or disable spaces inside a mention query (TiptapMentionInput only).

    Image drag/drop and paste are supported by pointing properties.s3PostPolicyRequestId at a request that returns an S3 presigned POST policy (e.g. AwsS3PresignedPostPolicy). The file handler is optional — omit the request id to disable uploads entirely.

    The blocks are registered in the default types map and are available out of the box on @lowdefy/server-dev. No private-registry tokens are required: the blocks use the open-source @tiptap/extension-file-handler instead of @tiptap-pro/extension-file-handler, so projects that migrated from a custom TipTap plugin can drop their TIPTAP_PRO_TOKEN environment variable and .npmrc scoped-registry config.

  • @lowdefy/server-e2e: ### Patch Changes

  • 596fddc: chore(connection-knex): update knex and SQL drivers; replace sqlite3 with better-sqlite3; replace mysql with mysql2.

    Bumped knex and its dialect drivers, and consolidated onto the actively-maintained drivers — replaced sqlite3 with better-sqlite3 and mysql with mysql2. Subsumes the prior [email protected] darwin-arm64 fix.

    @lowdefy/connection-knex dependency changes:

    • knex 2.5.13.2.9. Knex 3.x drops Node < 16; Lowdefy already requires Node 18+. The knex(config), .raw(), and dynamic query-builder API surface used by KnexRaw / KnexBuilder is unchanged.
    • pg 8.11.38.20.0.
    • Removed mssql. Knex's mssql dialect actually requires tedious (not the mssql package), and Lowdefy never imported mssql directly — it was only ever a vehicle for pulling tedious into the install tree. client: mssql in user YAML is unchanged: the knex client name stays the same, only the underlying npm package shipped with connection-knex changes.
    • Added tedious 19.2.1 as the SQL Server driver — the package knex actually loads when client: mssql is used.
    • Removed sqlite3. The driver is in maintenance-only mode upstream (the v6 release marked the repo unmaintained).
    • Added better-sqlite3 12.9.0 as the SQLite driver. Selectable as client: better-sqlite3 (or client: sqlite, which is now an alias of better-sqlite3 — see runtime client handling below).
    • Removed mysql. Unmaintained upstream since 2020.
    • Added mysql2 3.22.3 as the MySQL / MariaDB driver. Selectable as client: mysql2 in connection YAML.

    Runtime client handling (in createKnex):

    • client: sqlite is silently remapped to client: better-sqlite3. sqlite was historically a knex-level alias of sqlite3; this preserves the YAML alias while the underlying driver changes.
    • client: sqlite3 now throws a ConfigError with a migration message: Knex connection "client: sqlite3" is no longer supported. Use "client: better-sqlite3" or "client: sqlite" instead. Existing apps using client: sqlite3 need to update their connection YAML.
    • client: mysql now throws a ConfigError with a migration message: Knex connection "client: mysql" is no longer supported. Use "client: mysql2" instead. Existing apps using client: mysql need to update their connection YAML. mysql is not silently remapped because knex treats mysql and mysql2 as separate dialects with subtly different SQL formatters, not aliases — the migration is a deliberate user choice.

    pnpm.onlyBuiltDependencies allowlist for better-sqlite3:

    better-sqlite3 runs a native-binding install script (prebuild-install with a node-gyp rebuild fallback). pnpm 10 silently suppresses postinstall scripts for unapproved packages, which leaves the binding unbuilt and crashes KnexRaw / KnexBuilder at runtime.

    • Added better-sqlite3 to the allowlist on @lowdefy/server, @lowdefy/server-dev, and @lowdefy/server-e2e. These are the install roots in the CLI fetch flow under .lowdefy/{dev,build}/, where pnpm honors the per-package pnpm.onlyBuiltDependencies field.
    • Also added the same allowlist to the monorepo root package.json. The per-package field is ignored at workspace-root install (pnpm 10 only honors it on the install root), so contributors running pnpm install at the repo root would otherwise have to pnpm rebuild better-sqlite3 manually.
  • 762755c: feat(blocks-tiptap): Add new default block package with TiptapInput and TiptapMentionInput rich-text editors.

    @lowdefy/blocks-tiptap ships two rich-text editor blocks built on TipTap:

    • TiptapInput — standard rich-text editor with bold/italic/strike-through, multi-color highlight, headings, lists, tables, links, and a bubble menu.
    • TiptapMentionInput — everything TiptapInput does, plus an @-mention dropdown populated from a static options list or a Lowdefy request. Resolved mentions are returned on the block value as mentions: [...].

    Both blocks emit an object value shaped { html, text, markdown, fileList, mentions? } and register clear, setContent, and focus methods.

    Configurable extensions — defaults preserve the bundled editor; override any of these to trim the editor down or tune it:

    • properties.starterKit — object forwarded to TipTap StarterKit, e.g. { heading: false, codeBlock: false }.
    • properties.image{ enabled, maxWidth, zoom }
    • properties.table{ enabled, resizable }
    • properties.link{ enabled, autolink, linkOnPaste, openOnClick, defaultProtocol }
    • properties.highlight{ enabled, multicolor }
    • properties.mentions.char / properties.mentions.allowSpaces — change the trigger char (e.g. # for hashtags) or disable spaces inside a mention query (TiptapMentionInput only).

    Image drag/drop and paste are supported by pointing properties.s3PostPolicyRequestId at a request that returns an S3 presigned POST policy (e.g. AwsS3PresignedPostPolicy). The file handler is optional — omit the request id to disable uploads entirely.

    The blocks are registered in the default types map and are available out of the box on @lowdefy/server-dev. No private-registry tokens are required: the blocks use the open-source @tiptap/extension-file-handler instead of @tiptap-pro/extension-file-handler, so projects that migrated from a custom TipTap plugin can drop their TIPTAP_PRO_TOKEN environment variable and .npmrc scoped-registry config.

  • @lowdefy/logger: ### Patch Changes

  • e3fc007: fix(logger): Handle non-object JSON values in stdout line handler.

    JSON.parse can return null for literal "null" input, crashing the CLI log handler. Non-object parsed values are now treated as plain text lines.

v5.1.0 Breaking risk
⚠ Upgrade required
  • Migrate existing `DataDiff` usages to the new blocks in `@lowdefy/blocks-diff` using the provided mapping.
  • Update AgGrid columns to use the new `cell` object for built‑in renderers if leveraging number formatting, links, etc.
Breaking changes
  • Removal of `DataDiff` block from `@lowdefy/blocks-antd`; migrate to corresponding blocks in `@lowdefy/blocks-diff` (DiffList, DiffSideBySide, DiffTimeline, DiffGit).
Security fixes
  • Theme values interpolated into pre-hydration inline script are now fully escaped via `safeScriptJson`, closing six CodeQL `js/bad-code-sanitization` alerts.
Notable features
  • Dark mode polish: per-mode antd tokens (`lightToken`, `darkToken`, `lightComponents`, `darkComponents`) and pre-hydration script to eliminate white flash.
  • New package `@lowdefy/blocks-diff` with four diff blocks (DiffList, DiffSideBySide, DiffTimeline, DiffGit).
  • AgGrid built‑in cell renderers (`tag`, `avatar`, `link`, `date`, `boolean`, `progress`, `number`) and tooltip schema additions.
Full changelog

Highlights

  • Dark mode polish across the framework — new lightToken / darkToken and lightComponents / darkComponents keys on theme.antd let you override antd tokens per mode, a pre-hydration inline script paints the correct background before first paint (no more white flash on navigation), and the generated globals.css now ships themed scrollbars that auto-swap with the active theme.
  • New @lowdefy/blocks-diff package — Four dedicated blocks: DiffList, DiffSideBySide, DiffTimeline, and DiffGit.
  • New PageSidebarLayout block — full-page layout with a full-height collapsible sider, localStorage-persisted open state, mobile drawer, 8 content slots, and dark-mode-aware logo switching. PageSiderMenu also now persists its sider state (shared siderStorageKey).
  • AgGrid built-in cell renderers — every column now accepts a cell object with tag, avatar, link, date, boolean, progress, or number types, Excel-style number formatting via Intl.NumberFormat, row-data field paths (nameField, srcField, etc.), an ellipsis column helper, and a new onCellLink event. Tooltip props (tooltipField, tooltipValueGetter, enableBrowserTooltips) are now declared in the schemas, and loading overlays have been rewritten to fix a long-standing Safari stuck-overlay bug.
  • User-role operators_user.hasRole, _user.hasSomeRoles, and _user.hasAllRoles check the user.roles array and return a boolean, making role-gated UI and request auth easier to express.
  • selector cssKey on Select blocksSelector, MultipleSelector, and AutoComplete now expose a selector cssKey so you can cap the tag container height and scroll internally without reaching for Tailwind arbitrary variants or global CSS.
  • Security hardening — theme values interpolated into the pre-hydration <script> are now fully escaped (<, >, U+2028, U+2029) via a new safeScriptJson helper, closing six CodeQL js/bad-code-sanitization alerts.

What's New

feat(build): Themed default scrollbars in generated globals.css.

Packages: @lowdefy/build

Every Lowdefy app now ships with themed scrollbars out of the box. Native Windows/Linux scrollbars were rendering as light grey on dark surfaces (Modal, Drawer, overflowing containers), clashing with dark themes — macOS overlay scrollbars hid the problem. The generated globals.css now emits a @layer base block that:

  • Sets scrollbar-width: thin and scrollbar-color for Firefox and modern browsers.
  • Styles ::-webkit-scrollbar (10px, transparent track, subtle thumb with inset border, hover darkens) for Chromium / WebKit.
  • Drives all colors from antd CSS custom properties (--ant-color-border-secondary, --ant-color-text-tertiary) so they auto-swap on dark / light mode toggle.

User-provided CSS remains in @layer components, so any app-level ::-webkit-scrollbar overrides in public/styles.css still win.

feat(client): Per-mode theme tokens for dark/light customization.

Packages: @lowdefy/client, @lowdefy/server, @lowdefy/server-dev

theme.antd now accepts four new sibling keys so apps can soften base surfaces without juggling two theme files. Each is merged on top of the shared equivalent only when the matching mode is active:

  • lightToken / darkToken — override antd design tokens (e.g. colorBgLayout, colorBgContainer, colorBgElevated) per mode.
  • lightComponents / darkComponents — override component-level tokens per mode (e.g. Layout.siderBg, Layout.headerBg, Menu.darkItemBg) that aren't reachable via seed tokens.

The <html> pre-hydration inline script now reads darkToken.colorBgLayout / lightToken.colorBgLayout from the built theme, so the first paint matches your configured surface color with no flash of #000 or #fff.

theme:
  antd:
    token:
      colorPrimary: '#6366f1'
    darkToken:
      colorBgLayout: '#131419'
      colorBgContainer: '#1a1b22'
    darkComponents:
      Layout:
        headerBg: '#0e0f13'
        siderBg: '#0e0f13'
      Menu:
        darkItemBg: '#0e0f13'
        darkItemSelectedBg: '#252731'
  darkMode: system

Backwards compatible — apps that only use theme.antd.token keep antd's default base colors (dark #000, light browser-default).

feat(blocks-aggrid): Add built-in cell renderer types.

Packages: @lowdefy/blocks-aggrid

Every AgGrid column now accepts a cell object on columnDefs entries that selects a first-class renderer — tag, avatar, link, date, boolean, progress, number — plus an ellipsis: N column-level helper that auto-enables wrapText + autoHeight with an N-line clamp.

The number renderer wraps Intl.NumberFormat with Excel-style config: format (number / currency / percent / compact), locale, currency, decimals, accounting-style negative: parentheses, signColor (green/red by sign), and optional prefix / suffix. Number columns auto-right-align (cellStyle.justifyContent: flex-end + ag-right-aligned-header) and every cell.type supports an align: left | center | right override. Renderer output is React, vertically centred, and styled entirely through antd CSS tokens (--ant-control-height, --ant-margin-xs, --ant-color-*, --ant-border-radius, --ant-font-size, etc.) so the grid adapts to Material vs Balham row heights and to dark / compact antd theme.algorithm without per-theme overrides.

Field-valued keys (nameField, srcField, idField, colorFrom, and every value inside link.urlQuery) are plain row-data path strings — no _function wrapping required. Null values render a muted em-dash across every built-in type.

Link navigation: cell.type: link and avatar.link render anchors and emit a new onCellLink block event with the resolved link config; wire it to a Link action (params: { _event: link }) to navigate — matches the existing Lowdefy event → action pattern.

antd, @ant-design/icons, and dayjs are now declared as peer dependencies on @lowdefy/blocks-aggrid (de-facto required by the existing ag-grid-antd.module.css token mapping).

Also fixes the long-standing cell vertical-centering drift: .ag-cell is now a flex container via the antd theme CSS module, which also benefits users' existing renderHtml cells.

feat(blocks-aggrid): Declare tooltip properties in block schemas.

Packages: @lowdefy/blocks-aggrid

All six AgGrid variants (Alpine/Balham/Material for display and input) now declare enableBrowserTooltips, tooltipShowDelay, and tooltipHideDelay at the grid level and tooltipField, tooltipValueGetter, and tooltipComponent at the column level. These AG Grid props already worked — they were passed through via property spreading — but were not documented in the block schemas. Users can now discover and configure tooltips directly from the schema.

feat(blocks-antd): DataDiff gains sideBySide, timeline, and gitDiff modes, plus depth-aware nested rendering.

Packages: @lowdefy/blocks-antd

  • mode: sideBySide — two aligned antd Descriptions panels (Before / After) that respond to breakpoints.
  • mode: timeline — antd Timeline with one item per change, colour-coded and breadcrumb-labelled for context.
  • mode: gitDiff — unified-diff YAML patch rendering with +/− line markers; for technical users who want to see every line of change. Ignores the structured-rendering props (labels, format, maxDepth) but still honours hide / show.
  • list mode now sub-groups array-of-objects changes into per-item sections with their own summary chips, and breadcrumbs deeply-nested row labels (Order 1 › Customer › Name instead of just Name).
  • New maxDepth property (default 4) collapses changes deeper than the cap into a single JSON-rendered row at the cap, keeping deep payloads legible.

feat(blocks-antd): Add DataDiff block for rendering user-friendly diffs.

Packages: @lowdefy/blocks-antd

DataDiff compares two objects (before / after) and renders the differences using antd primitives (Descriptions, Collapse, Tag, Empty). v1 ships a polished list mode: grouped by top-level key, with +N / −N / ~N summary chips per group, icon- and color-coded change rows, and a collapsed-JSON fallback for entirely-new nested objects. All colors come from antd semantic tokens (colorSuccess, colorError, colorWarning) so the block respects dark mode and theme overrides automatically.

Per-path value formatters — date, datetime, boolean (Yes/No tag), currency (Intl.NumberFormat), json (pretty inside a subtle collapse), code, and enum (value → { label, color } map) — turn raw field values into something end-users can read. labels maps dotted paths to display names; hide / show accept exact paths, prefix.*, or *.leaf patterns. Built on microdiff (~1 kB, zero deps).

- id: order_audit
  type: DataDiff
  properties:
    before: _state.original
    after: _state.current
    labels:
      status: Order status
      total: Total
    format:
      total: { type: currency, currency: USD }
      status:
        type: enum
        map:
          pending: { label: Pending, color: warning }
          paid: { label: Paid, color: success }
    hide:
      - 'internal.*'

feat(blocks-antd): Expose selector cssKey on Select-based blocks.

Packages: @lowdefy/blocks-antd

Selector, MultipleSelector, and AutoComplete now expose a selector cssKey that targets the inner tag/value container (antd v6's content semantic slot, rendered as .ant-select-content in the DOM). Use it to cap the tag container height and enable internal scroll on multi-select blocks:

style:
  .selector:
    maxHeight: 96px
    overflowY: auto

Before this change, users had to reach for Tailwind arbitrary variants or global CSS (e.g. [&_.ant-select-selector]:max-h-24) to style the inner container, which leaked antd internals into app YAML and was brittle across antd upgrades.

feat(blocks-diff): New package. DataDiff extracted from blocks-antd and split

Packages: @lowdefy/blocks-antd, @lowdefy/blocks-diff

into DiffList, DiffSideBySide, DiffTimeline, and DiffGit blocks.

BREAKING: The DataDiff block has been removed from @lowdefy/blocks-antd.
Migrate to the per-mode blocks in @lowdefy/blocks-diff:

  • mode: listDiff.DiffList
  • mode: sideBySideDiff.DiffSideBySide
  • mode: timelineDiff.DiffTimeline
  • mode: gitDiffDiff.DiffGit

The diff, yaml, pluralize, and microdiff dependencies have been moved
from @lowdefy/blocks-antd to @lowdefy/blocks-diff along with the block.

feat: Add PageSidebarLayout block

Packages: @lowdefy/blocks-antd

New full-page layout block with a full-height sidebar, no top-level header, and mobile drawer navigation. The sider spans the entire viewport height with the logo pinned at the bottom.

PageSidebarLayout

  • Full-height collapsible sider with inline menu
  • Sider collapse state persists in localStorage (configurable key via siderStorageKey)
  • Dark mode via app-level ConfigProvider — all components adapt automatically via CSS variables
  • darkModeToggle, notifications, and profile properties shown in the sider on desktop and the mobile header on small screens, matching PageHeaderMenu and PageSiderMenu
  • theme property accepts an Ant Design design token object for fine-grained color customization via ConfigProvider
  • Responsive logo: full logo when sider is expanded, square logo when collapsed, auto-swaps between light and dark variants based on dark mode
  • 8 content slots: content, footer, header, siderOpen, siderClosed, mobileExtra, mobileDrawerContent, mobileDrawerFooter

Drawer

  • Added footer content slot and styles.footer passthrough

MobileMenu

  • Added logo property for drawer header branding with dark mode-aware logo switching
  • Added drawerContent and drawerFooter content slots
  • Changed category from display to container to support slot resolution

feat: Persist PageSiderMenu sider open state to localStorage

Packages: @lowdefy/blocks-antd

PageSiderMenu now persists its sider collapsed/expanded state across page navigations and reloads, matching PageSidebarLayout behavior.

  • Sider open state reads from and writes to lf-{siderStorageKey}-open in localStorage
  • New siderStorageKey property (default 'sider') — shares the same key as PageSidebarLayout by default, so the user's preference survives swapping between layouts
  • New sider.initialCollapsed property used as the fallback when no persisted value exists
  • Gracefully handles SSR and privacy-mode localStorage unavailability

Fixes & Improvements

  • fix(server): Prevent white flash on page navigation in dark mode. (@lowdefy/build, @lowdefy/client, @lowdefy/server, @lowdefy/server-dev)

    Pages no longer flash white when navigating between pages in dark mode. A synchronous inline script now sets the correct background color before the page paints, matching the user's dark mode preference from config, localStorage, or system settings.

  • feat(operators-js): Add _user.hasRole, _user.hasSomeRoles, and _user.hasAllRoles methods to check user roles against the user.roles array. hasRole takes a single role string; hasSomeRoles and hasAllRoles take an array of role strings. All return a boolean. (@lowdefy/docs, @lowdefy/operators-js)

  • fix(blocks-aggrid): React to loading prop changes on AgGrid. (@lowdefy/blocks-aggrid)

    The loading block flag now toggles AG Grid's native showLoadingOverlay / hideOverlay at runtime. Previously the overlay calls were inside a useEffect with an empty dependency array, so they only ran once on mount and never reacted to subsequent loading changes. The effect has been split in two — method registration still runs once, overlay toggling now runs whenever loading changes — and an onGridReady callback applies the initial overlay state safely after the grid api is attached.

  • fix(blocks-aggrid): Safari loading overlay stuck. (@lowdefy/blocks-aggrid)

    AgGrid and AgGridInput no longer use ag-grid's internal showLoadingOverlay / hideOverlay API to reflect the block's loading prop. On Safari / WebKit, a microtask race between our hideOverlay() call and ag-grid's own late showOverlay tick left the "Loading…" box stuck on screen even after data had rendered (ag-grid issues #4421, #1665, #8358). Chromium happened to win the race the other way, which hid the symptom.

    Both blocks now wrap AgGridReact in a position: relative div, set suppressLoadingOverlay on the grid, and render a small themed overlay component (LoadingOverlay.js) when the Lowdefy loading prop is true. The overlay is styled via antd CSS custom properties, so it follows the active theme.

  • fix(blocks-antd): PageSiderMenu state sync, setSiderOpen bug, and menu auto-popup. (@lowdefy/blocks-antd)

    Three related fixes so the sider persists correctly and the inline menu doesn't pop open flyouts on page load.

    • Persistence on hard-refresh / new tab: PageSiderMenu now feeds its computed openSiderState (read from localStorage['lf-{siderStorageKey}-open']) into the inner Sider block as initialCollapsed. Previously the Sider re-read the media-query-computed initialCollapsed independently and ignored the persisted value, so the sider always started collapsed on desktop regardless of the user's preference.
    • setSiderOpen({ open }) bug: the action called the Sider's _toggleSiderOpen (a no-arg toggle) with an { open } argument that was silently ignored. Fixed to call _setSiderOpen({ open }) so the sider lands in the requested state. toggleSiderOpen now also uses the explicit setter with the computed next value for symmetry.
    • Menu auto-popup when sider is collapsed: the Menu block's defaultOpenKeys guard checked properties.collapsed !== true, but PageSiderMenu never passes collapsed as a prop — antd derives the collapsed state from SiderContext. As a result the current page's parent group was added to defaultOpenKeys, and antd's Menu auto-popped the flyout for that group on mount. Menu now reads siderCollapsed from Layout._InternalSiderContext (the same channel antd's own Menu uses) so the group auto-expansion only applies when the sider is expanded.
  • fix(server): Escape theme values embedded in the pre-hydration inline script. (@lowdefy/server, @lowdefy/server-dev)

    _document.js interpolates configColorMode, darkToken.colorBgLayout, and lightToken.colorBgLayout from theme.json into a synchronous <script> block to set the <html> background before hydration. Previously the values went through JSON.stringify only — enough to escape JS-string-context characters, but not enough to prevent a value containing </script> (or U+2028 / U+2029 line separators) from breaking out of the enclosing <script> tag.

    Added a safeScriptJson helper that additionally escapes <, >, control chars, and U+2028 / U+2029 to \uXXXX sequences after JSON.stringify. For every valid color value (#1e293b, rgb(...), slategray, oklch(...), etc.) the output is byte-identical to the previous behavior; only payloads that would have tripped <script> breakout or JS-line-terminator injection are now neutralized.

    Closes the six js/bad-code-sanitization CodeQL alerts (89 – 94) opened against the per-mode-theme PR.

v5.0.0 Breaking risk
⚠ Upgrade required
  • Migrate all _moment usages to _dayjs; update format strings if necessary
  • Review date picker configurations for day.js compatibility
  • Check Google Sheets connections and AG Grid cell renderers for date handling changes
Breaking changes
  • _moment operator removed; use _dayjs instead
  • @lowdefy/operators-moment package removed
  • Nunjucks `date` filter now uses day.js (format strings must be day.js‑compatible)
Notable features
  • Theme tokens at runtime via _theme operator and custom token config
  • Keyboard shortcuts on block events with platform‑aware bindings
  • System dark mode support through theme.darkMode config
Full changelog

Highlights

  • Theme tokens at runtime: Access Ant Design v6 design tokens (colors, spacing, typography) with the new _theme operator. Configure custom tokens via theme.antd.token and theme.antd.algorithm in lowdefy.yaml for theme-aware component styling.
  • Keyboard shortcuts on events: Add shortcut property to block events for platform-aware keyboard bindings (e.g., mod+K → Cmd+K on Mac, Ctrl+K on Windows). Supports key sequences and multiple bindings. Built-in validation warns about duplicates and browser conflicts. Button, Anchor, Tag, and Search blocks display shortcut badges.
  • System dark mode support: New theme.darkMode: 'system' | 'light' | 'dark' config auto-follows OS preference. SetDarkMode action accepts string params to control mode. _media: darkModePreference returns user preference; _media: darkMode continues returning the boolean state.
  • Search command palette block: New Search display block provides full-text search with MiniSearch. Supports static JSON index via indexUrl or runtime indexing with documents. Features grouped results, keyboard navigation, term highlighting, and search history. Replaces Algolia DocSearch with self-hosted alternative.
  • lowdefy upgrade command: New CLI tool guides version migrations with prompt-based codemods. Use --to, --plan, or --resume to manage upgrades. Each prompt can be copied to clipboard, viewed as a manual guide, or skipped. Upgrade state persists for interrupted migrations. v5.0 codemods cover antd v6, layout grid, and dayjs migrations.
  • Better build error messages: Schema validation errors now include property names. CSS errors suggest dot-prefixed slot keys. YAML parse errors surface immediately instead of crashing on null entries.
  • AG Grid automatic theming: All six grid blocks now follow Ant Design theme automatically, responding to light/dark mode and custom colors without configuration. Removed separate dark variant blocks. Override individual --ag-* variables via the block's style property.
  • Dark mode toggle in headers: New darkModeToggle: true property on PageHeaderMenu and PageSiderMenu renders a built-in sun/moon toggle. Preference persists to localStorage and respects OS default. toggleDarkMode method available for programmatic control.
  • Header customization: Add color property to set header background (defaults to --ant-color-bg-container). New iconsColor property controls notification, profile, and dark mode toggle icon colors — useful with dark backgrounds.
  • PhoneNumberInput formatting: Phone number values now strip leading zeros and non-digits. 0821234567 with +27 selected produces +27821234567 instead of +270821234567.
  • S3 upload improvements: New onBeforeUpload event fires before each file upload; throw in the handler to cancel. XHR uploads are now Promise-based with proper error propagation. CORS/network failures throw ServiceError with diagnostic message. File metadata serialized to plain objects to preserve _event references.
  • Modulo operator: Added _math.mod for remainder calculations. Use _math.mod: [10, 3] or _math.mod: { dividend: 10, divisor: 3 }.
  • _date operator flexibility: Now accepts Date objects in addition to numbers and strings.
  • Anchor icon spacing: Fixed icon margin issue so icons no longer sit flush against title text.
  • Error serialization robustness: Fixed crashes on circular references in error objects (e.g., Axios responses with Node.js request cycles). Errors now log properly instead of crashing the logger.
  • ⚠️ BREAKING: Block metadata restructure — Move block definitions from schema.js to meta.js. Block packages export ./metas instead of ./schemas. Components no longer have .meta static property. Metadata loaded from blockMetas.json build artifact at runtime. Use buildBlockSchema(meta) from @lowdefy/block-utils to generate JSON Schema.
  • ⚠️ BREAKING: Moment.js removed — Replace _moment operator with _dayjs. Apps using _moment must migrate. @lowdefy/operators-moment package removed. Date picker blocks use day.js internally. Nunjucks date filter uses day.js. humanizeDuration thresholds parameter is now ignored.
  • ⚠️ BREAKING: Component theme properties removed — Remove header.theme, sider.theme, menu.theme properties from PageSiderMenu, PageHeaderMenu, Header, and Sider blocks. Dark mode now automatic via CSS variables. Also removed header.style, sider.style, etc. — use style: { .header }, style: { .sider } instead. Removed onProfileClick and onNotificationClick events; use notifications.link property instead. collapsible and initialCollapsed properties removed from PageSiderMenu.
  • ⚠️ BREAKING: Algolia blocks removed — Use the Search block in @lowdefy/blocks-antd instead of @lowdefy/blocks-algolia.

What's New

Add theme token system. Use _theme operator to access Ant Design v6 design tokens (colors, spacing, typography) at runtime. Theme is configured via theme.antd.token and theme.antd.algorithm in lowdefy.yaml. The _theme operator resolves the full computed token set including antd defaults.

Packages: @lowdefy/api, @lowdefy/build, @lowdefy/build, @lowdefy/build, @lowdefy/build, @lowdefy/build, @lowdefy/build, @lowdefy/client, @lowdefy/client, @lowdefy/client, @lowdefy/client, @lowdefy/client, @lowdefy/docs, @lowdefy/engine, @lowdefy/engine, @lowdefy/layout, @lowdefy/layout, @lowdefy/layout, @lowdefy/actions-core, @lowdefy/actions-pdf-make, @lowdefy/blocks-aggrid, @lowdefy/blocks-aggrid, @lowdefy/blocks-aggrid, @lowdefy/blocks-antd, @lowdefy/blocks-antd, @lowdefy/blocks-antd, @lowdefy/blocks-antd, @lowdefy/blocks-antd, @lowdefy/blocks-antd, @lowdefy/blocks-antd, @lowdefy/blocks-antd, @lowdefy/blocks-antd, @lowdefy/blocks-antd, @lowdefy/blocks-antd, @lowdefy/blocks-antd, @lowdefy/blocks-antd, @lowdefy/blocks-antd, @lowdefy/blocks-antd, @lowdefy/blocks-basic, @lowdefy/blocks-basic, @lowdefy/blocks-basic, @lowdefy/blocks-echarts, @lowdefy/blocks-echarts, @lowdefy/blocks-echarts, @lowdefy/blocks-echarts, @lowdefy/blocks-google-maps, @lowdefy/blocks-google-maps, @lowdefy/blocks-google-maps, @lowdefy/blocks-loaders, @lowdefy/blocks-loaders, @lowdefy/blocks-loaders, @lowdefy/blocks-markdown, @lowdefy/blocks-markdown, @lowdefy/blocks-markdown, @lowdefy/blocks-qr, @lowdefy/blocks-qr, @lowdefy/blocks-qr, @lowdefy/connection-axios-http, @lowdefy/connection-elasticsearch, @lowdefy/connection-google-sheets, @lowdefy/connection-knex, @lowdefy/connection-redis, @lowdefy/connection-sendgrid, @lowdefy/connection-stripe, @lowdefy/connection-test, @lowdefy/operators-change-case, @lowdefy/operators-diff, @lowdefy/operators-js, @lowdefy/operators-js, @lowdefy/operators-jsonata, @lowdefy/operators-mql, @lowdefy/operators-nunjucks, @lowdefy/operators-uuid, @lowdefy/operators-yaml, @lowdefy/plugin-auth0, @lowdefy/plugin-aws, @lowdefy/plugin-aws, @lowdefy/plugin-aws, @lowdefy/plugin-csv, @lowdefy/plugin-next-auth, @lowdefy/server, @lowdefy/server, @lowdefy/server-dev, @lowdefy/server-dev, @lowdefy/server-dev, @lowdefy/server-dev, @lowdefy/block-dev, @lowdefy/block-dev, @lowdefy/block-utils, @lowdefy/block-utils, @lowdefy/e2e-utils, @lowdefy/node-utils

Restructure block metadata from component static properties to dedicated meta.js files.

Packages: @lowdefy/build, @lowdefy/client, @lowdefy/engine, @lowdefy/blocks-aggrid, @lowdefy/blocks-antd, @lowdefy/blocks-basic, @lowdefy/blocks-echarts, @lowdefy/blocks-google-maps, @lowdefy/blocks-loaders, @lowdefy/blocks-markdown, @lowdefy/blocks-qr, @lowdefy/plugin-aws, @lowdefy/server-dev, @lowdefy/block-utils

Breaking Changes

  • schema.js renamed to meta.js: Block definitions moved from schema.js to meta.js. The meta.js files export category, icons, valueType, cssKeys, events, and properties (JSON Schema).
  • schemas.js barrel renamed to metas.js: Block packages export ./metas instead of ./schemas.
  • .meta removed from components: Block components no longer have a .meta static property. Metadata is loaded from the blockMetas.json build artifact at runtime.
  • blockMetas.json build artifact: The build pipeline writes plugins/blockMetas.json containing category, valueType, and initValue for each block type.
  • buildBlockSchema(meta): New function in @lowdefy/block-utils generates complete JSON Schema from meta objects with operator support and CSS slot key validation.

Replace moment.js with day.js across the monorepo.

Packages: @lowdefy/build, @lowdefy/blocks-antd, @lowdefy/connection-google-sheets, @lowdefy/operators-dayjs, @lowdefy/nunjucks

Breaking Changes

  • _moment operator removed: Use _dayjs instead. The new @lowdefy/operators-dayjs package provides the _dayjs operator with the same API patterns.
  • @lowdefy/operators-moment package removed: Apps using _moment must migrate to _dayjs.
  • Nunjucks date filter: Now uses day.js internally. Format strings are day.js compatible (mostly identical to moment).
  • Date picker blocks: All date/time picker blocks use day.js instead of moment for value parsing and formatting.
  • Google Sheets connection: Date serialization uses day.js internally.
  • humanizeDuration thresholds: The thresholds parameter on _dayjs.humanizeDuration is silently ignored (day.js does not support it).
  • AgGrid cell renderers: Update __moment to __dayjs in custom AG Grid cell renderer references.
  • Date selector UTC handling: Antd v6 bundles its own dayjs without the UTC plugin. Date selector blocks wrap antd's dayjs instances with the extended dayjs before calling .utc() — this is handled internally and requires no user action.

Add keyboard shortcut support for block events.

Packages: @lowdefy/build, @lowdefy/client, @lowdefy/engine, @lowdefy/blocks-antd, @lowdefy/blocks-basic, @lowdefy/block-utils

Blocks can now define keyboard shortcuts on events using the shortcut property in the event long-form object. Shortcuts are platform-aware (mod+K maps to Cmd+K on Mac, Ctrl+K on Windows), support sequences (g i), and can be arrays for multiple bindings.

  • Build validation warns on duplicate shortcuts within a page and conflicts with browser defaults (e.g. mod+N)
  • ShortcutManager registers a single global keydown listener via tinykeys with visibility gating and input field suppression
  • ShortcutBadge component renders platform-appropriate key symbols (e.g. ⌘ K) and is available to all blocks via components.ShortcutBadge
  • ShortcutBadge in blocks: Button, Anchor, Tag, and Search blocks display a platform-aware keyboard shortcut badge (e.g. ⌘S / Ctrl+S) next to the title when the event has a shortcut defined

Add theme.darkMode config with system preference support.

Packages: @lowdefy/build, @lowdefy/client, @lowdefy/actions-core, @lowdefy/blocks-antd, @lowdefy/operators-js, @lowdefy/server, @lowdefy/server-dev

System Dark Mode (theme.darkMode)

  • New theme.darkMode config key accepts 'system' (default), 'light', or 'dark'
  • When set to 'system', the app follows the OS dark mode preference and updates live when it changes
  • When set to 'light' or 'dark', the developer locks the mode — user preferences are stored but not applied

SetDarkMode Action

  • Now accepts string params: darkMode: 'system' | 'light' | 'dark'
  • Without params, cycles through light, dark, and system preferences

_media Operator

  • New _media: darkModePreference returns the user's preference ('system', 'light', or 'dark')
  • _media: darkMode continues to return the effective boolean state

Dark Mode Rendering

  • Notification, Message, and ConfirmModal render with correct dark mode colors via App.useApp() hooks
  • Loader blocks (Skeleton, Spinner) use antd design tokens instead of hardcoded colors
  • 404 page and loading states use theme-aware backgrounds
  • Mobile menu drawer background matches the active theme

feat: Add lowdefy upgrade command with prompt-based codemod system

Packages: lowdefy, @lowdefy/codemods

New CLI command that guides version migrations using markdown prompts. Resolves a chain of upgrade phases from current to target version, presents migration prompts in order, and tracks progress for --resume support.

CLI (lowdefy)

  • lowdefy upgrade command with --to, --plan, --resume options
  • Version chain resolver computes ordered upgrade phases from semver ranges
  • Fetches @lowdefy/codemods package from npm, presents migration prompts
  • Each prompt can be copied to clipboard for AI tools, viewed as a manual guide, or skipped
  • Upgrade state persistence in .lowdefy/upgrade-state.json for interrupted upgrades
  • Build-time warning when skipped codemods are detected

Codemods (@lowdefy/codemods)

  • v5.0 entry with 20 migration prompts
  • Covers antd v6 upgrade (14 prompts), layout grid migration (4 prompts), dayjs migration (2 prompts)
  • Self-contained markdown prompts with context, examples, edge cases, and verification steps

AG Grid blocks now follow the Ant Design theme automatically. All six grid blocks (AgGridAlpine, AgGridBalham, AgGridMaterial, and their Input variants) map ag-grid CSS variables to antd design tokens, so they respond to light/dark mode and custom theme colors without any configuration. Override individual --ag-* variables via the block's style property for per-instance customization. The explicit dark variant blocks (AgGridAlpineDark, AgGridBalhamDark, AgGridInputAlpineDark, AgGridInputBalhamDark) have been removed.

Packages: @lowdefy/blocks-aggrid

Remove per-component header.theme, sider.theme, and menu.theme string properties from PageSiderMenu, PageHeaderMenu, Header, and Sider blocks. Dark mode now works automatically via CSS variables from the root ConfigProvider — no manual theme switching needed.

Packages: @lowdefy/blocks-antd

Removed properties:

| Block | Removed Property |
| -------------- | ------------------------- |
| PageSiderMenu | properties.header.theme |
| PageSiderMenu | properties.sider.theme |
| PageSiderMenu | properties.menu.theme |
| PageHeaderMenu | properties.header.theme |
| PageHeaderMenu | properties.menu.theme |
| Header | properties.theme |
| Sider | properties.theme |

Migration: Simply remove these properties. Dark mode is handled automatically by the global ConfigProvider. Use darkModeToggle: true on page blocks or SetDarkMode action for user-facing toggle. Use properties.theme (design token object) for fine-grained color customization.

Also removed:

| Block | Removed Property | Replacement |
| -------------- | -------------------------------- | --------------------------- |
| PageSiderMenu | properties.header.style | style: { .header } |
| PageSiderMenu | properties.header.contentStyle | style: { .headerContent } |
| PageSiderMenu | properties.sider.style | style: { .sider } |
| PageSiderMenu | properties.footer.style | style: { .footer } |
| PageSiderMenu | properties.content.style | style: { .content } |
| PageSiderMenu | properties.logo.style | style: { .logo } |
| PageHeaderMenu | properties.header.style | style: { .header } |
| PageHeaderMenu | properties.header.contentStyle | style: { .headerContent } |
| PageHeaderMenu | properties.footer.style | style: { .footer } |
| PageHeaderMenu | properties.content.style | style: { .content } |
| PageHeaderMenu | properties.logo.style | style: { .logo } |

Events removed:

| Block | Removed Event | Replacement |
| -------------- | --------------------- | ----------------------------------------- |
| PageSiderMenu | onNotificationClick | Use notifications.link property instead |
| PageSiderMenu | onProfileClick | Removed |
| PageHeaderMenu | onNotificationClick | Use notifications.link property instead |
| PageHeaderMenu | onProfileClick | Removed |

Other removals:

  • collapsible and initialCollapsed properties removed from PageSiderMenu sider
  • Horizontal menu border removed from PageHeaderMenu header

Added:

  • notifications.link property for notification item navigation

Add darkModeToggle property to PageHeaderMenu and PageSiderMenu. Set darkModeToggle: true to render a built-in sun/moon toggle button in the header that switches between light and dark Ant Design themes. The preference is persisted to localStorage and respects the OS dark mode setting as default. A toggleDarkMode method is also registered for programmatic control.

Packages: @lowdefy/blocks-antd

feat(plugin-aws): Add onBeforeUpload event and improve S3 upload error handling.

Packages: @lowdefy/plugin-aws

S3UploadButton and S3UploadDragger now fire an onBeforeUpload event before each file upload begins. If any action in the event handler throws, the upload is cancelled — useful for file validation, size checks, or confirmation prompts.

Upload error handling has been rewritten: XHR uploads are now Promise-based with proper error propagation, CORS/network failures throw a ServiceError with a diagnostic message, and file metadata is serialized into a plain object so _event resolution no longer destroys File/Blob references.

Fixes & Improvements

  • Improve build error messages: schema validation errors include the property name, style/class errors suggest dot-prefixed CSS slot keys, and YAML parse errors surface immediately instead of crashing on null entries. (@lowdefy/build, @lowdefy/operators-js)

  • feat(blocks-antd): Add Search command palette block with MiniSearch. (@lowdefy/build, @lowdefy/blocks-antd)

    New Search display block provides a full-text search command palette (Cmd+K / Ctrl+K) using MiniSearch (~6KB) and antd Modal.

    • Pre-built index support: Load a static JSON index via indexUrl for zero-config search on static sites
    • Runtime indexing: Pass documents array with fields and storeFields for client-side indexing
    • Grouped results: Results auto-grouped by configurable field with section headers
    • Keyboard navigation: Arrow keys, Enter to select, Escape to close
    • Term highlighting: Matched search terms highlighted in results
    • Recent searches: localStorage-backed search history with configurable count
    • 14 CSS slots: Full style customization via styles/classNames (trigger, modal, input, results, groups, highlights)
    • Analytics-friendly events: onSelect passes the result item, search query, and resultCount for click-through tracking; onSearch passes the search term and result count on each query change

    Docs app integration

    • New search index transformer (generateSiteAssets.js) builds a MiniSearch index at build time from page content
    • Replaces Algolia DocSearch with the self-hosted Search block — removes external CDN dependency

    Removed

    • @lowdefy/blocks-algolia package has been removed. Use the Search block in @lowdefy/blocks-antd instead.
  • fix(blocks-antd): Format PhoneNumberInput phone_number value. (@lowdefy/blocks-antd)

    PhoneNumberInput now strips leading zeros and non-digit characters from user input when building the phone_number value. Typing 0821234567 with +27 selected now produces +27821234567 instead of +270821234567. Empty input produces an empty string instead of just the dial code.

  • Add color and iconsColor properties to Header block. (@lowdefy/blocks-antd)

    Set color to change the header background color (defaults to --ant-color-bg-container). Set iconsColor to control the color of notification, profile, and dark mode toggle icons — useful when using a dark background color. The iconsColor property is also available on PageHeaderMenu and PageSiderMenu.

  • Fix Anchor block icon spacing by adding marginRight: 4 to the icon style so the icon doesn't sit flush against the title text. (@lowdefy/blocks-basic)

  • feat(operators-js): Add _math.mod modulo operator. (@lowdefy/operators-js)

    Added _math.mod operator for modulo (remainder) calculations. Supports both array and named argument forms: _math.mod: [10, 3] or _math.mod: { dividend: 10, divisor: 3 }.

  • fix(operators-js): The _date operator now accepts Date objects as input, in addition to numbers and strings. (@lowdefy/operators-js)

  • refactor(plugin-aws): Migrate S3 presigned URL operations from deprecated aws-sdk v2 to modular @aws-sdk v3. (@lowdefy/plugin-aws)

    Replaced the monolithic aws-sdk package with the modular v3 packages (@aws-sdk/client-s3, @aws-sdk/s3-request-presigner, @aws-sdk/s3-presigned-post). No changes to the request/connection API — existing configs work without modification.

  • fix(helpers): Fix error serialization crash on circular structures. (@lowdefy/helpers)

    Errors with circular references in nested objects (e.g., Axios HTTP error responses containing Node.js request/response cycles) crashed the logger with "Converting circular structure to JSON" instead of logging the actual error. extractErrorProps now deep-cleans plain objects, arrays, and non-Error causes — stripping class instances, detecting circular references, and capping object depth.

v4.7.3 Security relevant
Security fixes
  • Validate session.user.roles as an array of strings to prevent silent authorization bypasses caused by misconfigured auth.userFields mappings
Notable features
  • Dev server skeleton rebuild detection uses build's ref map for accuracy
  • Runtime operators _date, _intl, and _number.toLocaleString evaluate at runtime
Full changelog

Highlights

  • Session role validation: Session roles are now validated as arrays during session assembly, catching misconfigured auth.userFields mappings early with a clear error message. Prevents silent authorization bypasses from incorrect role configuration.

  • Dev server skeleton rebuild detection: Fixed dev server to more accurately detect which file changes require skeleton rebuilds, using the build's ref map instead of file paths. Prevents unnecessary rebuilds when modifying page templates and catches changes to API endpoints.

  • Runtime operators no longer frozen at build time: Fixed _date, _intl, and _number.toLocaleString operators from being evaluated during build, which was freezing their values instead of evaluating them at runtime with the correct context.

Fixes & Improvements

  • fix(api): Validate session.user.roles is an array of strings. (@lowdefy/api)

    Misconfigured auth.userFields mapping roles to a non-array provider field (e.g., a string) caused silent authorization bypasses via String.prototype.includes substring matching. Session roles are now validated after session assembly, throwing a clear ConfigError pointing to the auth configuration. Added a defense-in-depth guard in createAuthorize for the same check.

  • fix(build,server-dev): Improved accuracy of dev server skeleton rebuild detection. (@lowdefy/build, @lowdefy/server-dev)

    The dev server previously used a path-based heuristic to decide which file changes required a skeleton rebuild. This could miss changes to API endpoints referenced from page directories, and unnecessarily rebuild for non-skeleton page templates. Skeleton rebuild classification now uses the build's ref map as the source of truth, ensuring only the correct file changes trigger skeleton rebuilds.

  • fix: Prevent _date, _intl, and _number.toLocaleString operators from being evaluated at build time. (@lowdefy/operators-js)

    These operators depend on runtime context (current date/time, locale) and were incorrectly marked as static, causing them to be evaluated during the build and freezing their values.

v4.7.2 Bug fix

Fixed dev server hang when a page YAML file's entire content was a `_ref`.

Full changelog

Highlights

  • Fixed dev server hang that occurred when a page's entire YAML content was a _ref — the server could get stuck indefinitely at "Building config..."
  • Improved custom plugin type map generation to handle missing lowdefy.yaml files

Fixes & Improvements

  • fix: Fix dev server build hang when page files contain top-level _ref. (@lowdefy/build, @lowdefy/server, @lowdefy/server-dev)

    The dev server could hang indefinitely at "Building config..." when a page YAML file's entire content was a _ref. This caused a self-referencing parent in the ref map, leading to an infinite loop during page source resolution. Also fixed null lowdefy.yaml handling in custom plugin type map generation.

v4.7.1 New feature
⚠ Upgrade required
  • Rename MDB_E2E_URI to LOWDEFY_E2E_MONGODB_URI in scaffold templates and run dependency install manually after init
  • Page navigation now waits for 'domcontentloaded' instead of full 'load' event
Notable features
  • Dev server dynamically discovers and serves icons referenced in page blocks without rebuild or restart
  • Date picker e2e helpers (do.select, do.fill) added with time selection for DateTimeSelector
  • E2E secrets can be overridden via LOWDEFY_E2E_SECRET_* env vars
Full changelog

Highlights

  • Dev server: Icons referenced in page blocks are now dynamically discovered and served without requiring a rebuild or server restart.

  • Date picker e2e testing: All date picker blocks now support do.select() for calendar interaction and do.fill() for typing dates directly. DateTimeSelector also supports time selection.

  • Operators: Fixed _function callback mutation in evaluateOperators that was causing _build.array.map and similar operators to produce duplicate results on repeated invocations.

  • E2E testing: Environment variable LOWDEFY_E2E_SECRET_* can now override LOWDEFY_SECRET_* values, allowing test infrastructure (e.g., MongoMemoryServer) to coexist with secret managers.

  • E2E navigation: Page navigation now waits for domcontentloaded instead of the full load event, preventing hangs on pages with WebSocket connections or slow resources.

  • E2E utils: Renamed MDB_E2E_URI to LOWDEFY_E2E_MONGODB_URI in scaffold templates. Init script no longer auto-installs dependencies — they're added to package.json and the user is prompted to install.

  • Build performance: Sibling refs are now resolved in parallel to interleave CPU and I/O operations.

Fixes & Improvements

  • fix(build): Dev server dynamically loads icons discovered during JIT page builds. (@lowdefy/build, @lowdefy/server-dev)

    Icons referenced only inside page blocks (e.g., icon: FiAperture on a Button) were not available in the dev server's static bundle, causing a fallback icon to render. The JIT page builder now detects missing icons when a page is compiled, extracts their SVG data from react-icons, and serves it via a dynamic API endpoint. The client fetches and merges these icons at runtime without triggering a Next.js rebuild or server restart.

  • Resolve sibling refs in parallel using Promise.all to interleave CPU and I/O during build. (@lowdefy/build)

  • feat(blocks-antd): Add do.select() and do.fill() to date picker e2e helpers. (@lowdefy/blocks-antd)

    All five date picker e2e helpers (DateSelector, DateTimeSelector,
    DateRangeSelector, MonthSelector, WeekSelector) now support
    do.select() for calendar UI interaction and do.fill() for
    typing dates directly. DateTimeSelector also supports time
    selection via the time panel.

  • Fix _function callback template being mutated in-place by evaluateOperators, causing _build.array.map and similar operators to produce duplicate results from repeated callback invocations. (@lowdefy/operators-js)

  • feat(server-e2e): Add LOWDEFYE2E_SECRET* override support. (@lowdefy/server-e2e)

    Secrets can now be overridden in e2e tests using LOWDEFY_E2E_SECRET_* environment variables. These take precedence over LOWDEFY_SECRET_* values, allowing test infrastructure (e.g. MongoMemoryServer) to coexist with secret managers injected via commandPrefix.

  • fix(e2e-utils): Use domcontentloaded for page navigation. (@lowdefy/e2e-utils)

    Page navigation now uses waitUntil: 'domcontentloaded' instead of the default load event. This prevents hangs on pages with WebSocket connections or slow-loading resources, since the Lowdefy client readiness check is already a stronger signal.

  • refactor(e2e-utils): Update scaffold env vars and simplify init. (@lowdefy/e2e-utils)

    Renamed MDB_E2E_URI to LOWDEFY_E2E_MONGODB_URI in scaffold templates to align with the new LOWDEFY_E2E_SECRET_* override pattern. The init script no longer runs install automatically — dependencies are added to package.json and the user is prompted to install.

v4.7.0 Breaking risk
Notable features
  • Single-pass async walker (`walker` module) replaces multi‑pass JSON serialization in `@lowdefy/build`, reducing serializer.copy calls per ref.
  • New e2e API assertions via `ldf.api()` with methods `expect.toFinish()`, `expect.toHaveResponse()`, `expect.toHavePayload()`, `response()`, and `state()`.
Full changelog

Highlights

  • Build Performance: Single-pass async walker replaces multi-pass JSON serialization for ref resolution, eliminating 5+ serializer.copy calls per ref
  • Build Errors: Collect and report all ref errors at once instead of stopping on the first failure — faster fix-rebuild cycles when multiple config files have issues
  • Inline Pages: Fixed inline page content being stripped during JIT builds with no recovery path
  • Template Errors: YAML errors in .yaml.njk templates now show "Nunjucks template produced invalid YAML" instead of misleading line numbers
  • CLI: Build now properly exits with process.exit(1) on errors instead of hanging with a spinner
  • CLI Start: Fixed port availability check when no --port flag is passed (now defaults to port 3000)
  • Build Logs: Removed spurious print: warn fields from build logger output
  • E2E Testing: New ldf.api() assertions for API endpoint testing — expect.toFinish(), expect.toHaveResponse(), expect.toHavePayload(), response(), state()
  • E2E Developer Experience: New scaffold scripts (e2e:headed, e2e:server), SLOW_MO env var support, and fixed template defaults (appDir, health check, fixtures)
  • E2E Block IDs: Dotted block IDs (e.g., form.field.name) now work correctly in CSS selectors with new escapeId() utility
  • Helpers: Fixed makeReplacer mutating object marker enumerability, preventing internal markers from leaking to plugin components

What's New

feat: Single-pass async walker for ref resolution

Packages: @lowdefy/build, @lowdefy/operators, @lowdefy/helpers

Single-Pass Walker (@lowdefy/build)

  • New walker module replaces the multi-pass JSON round-trip architecture in buildRefs with a single async tree walk
  • Resolves _ref markers, evaluates _build.* operators, and tags ~r provenance in one pass instead of 5+ serializer.copy calls per ref
  • Wired into both buildRefs (production) and buildPageJit (dev server)
  • Added isPageContentPath for semantic shallow build matching, replacing brittle path-index checks
  • Deleted redundant code replaced by walker: getRefsFromFile, populateRefs, createRefReviver, and the evaluateStaticOperators wrapper

In-Place Operator Evaluation (@lowdefy/operators)

  • New evaluateOperators function walks a tree in-place and evaluates operator nodes, avoiding JSON serialization round-trips
  • Used by the walker module to evaluate _build.* operators inline during ref resolution

Serializer Fix (@lowdefy/helpers)

  • Added skipMarkers option to serializer.serializeToString to exclude internal markers (~k, ~r, ~l, ~arr) from serialized output

feat(e2e-utils): Add ldf.api() assertions for API endpoint testing

Packages: @lowdefy/e2e-utils

  • New api.js core module with getApiState, getApiResponse, expectApi functions
  • Reads from window.lowdefy.apiResponses[endpointId][0] (mirrors request pattern)
  • ldf.api(endpointId).expect.toFinish() — wait for API call completion
  • ldf.api(endpointId).expect.toHaveResponse(response) — assert response
  • ldf.api(endpointId).expect.toHavePayload(payload) — assert sent payload
  • ldf.api(endpointId).response() — get raw response value
  • ldf.api(endpointId).state() — get full API state object
  • ldf.mock.api() now captures payloads for assertion
  • ldf.mock.getCapturedApi(endpointId) — retrieve captured API data

Fixes & Improvements

  • fix(build): Report all ref errors at once instead of stopping on the first one. (@lowdefy/build)

    When multiple referenced files have errors (missing files, YAML parse errors, invalid refs), the build now collects and reports all errors at once instead of stopping on the first failure. This reduces the fix-rebuild-fix cycle when multiple config files have issues.

  • fix(build): Preserve inline page content in JIT builds (@lowdefy/build, lowdefy, @lowdefy/docs)

    Pages declared inline in lowdefy.yaml (not via _ref) had their content stripped during shallow builds with no way to recover at JIT time, resulting in empty page shells. Detect inline pages by checking refId matches root ref with no sourceRef, and skip stripping. Set refId to null for inline pages in createPageRegistry so buildPageJit reads the pre-built artifact instead of attempting JIT resolution.

  • fix(build): Improve error message for YAML errors in njk templates (@lowdefy/build)

    When a .yaml.njk nunjucks template produces invalid YAML, the error now says "Nunjucks template produced invalid YAML" instead of showing a misleading line number from the generated output.

  • fix(cli): Exit process and stop spinner on build errors. (lowdefy)

    The CLI error handler logged errors but never called process.exit(1), so the process continued running with a spinning indicator after a build failure. Added process.exit(1) to runCommand after error handling, and added { spin: 'fail' } to stop the spinner in runLowdefyBuild, runNextBuild, and installServer catch blocks.

  • fix(cli): Fix port availability check for start command (lowdefy, @lowdefy/server-dev)

    The CLI's checkPortAvailable was called with undefined port when no --port flag was passed, causing net.listen(undefined) to bind a random port instead of checking port 3000. Added default port: 3000 in getOptions. Removed redundant checkPortAvailable from server-dev manager since the CLI now catches port conflicts before the server starts.

  • fix(e2e-utils): Escape dotted block IDs in e2e CSS selectors. (@lowdefy/blocks-antd, @lowdefy/blocks-basic, @lowdefy/e2e-utils)

    Block IDs containing dots (e.g., form.field.name) now work correctly in e2e test locators. Added escapeId() utility to @lowdefy/e2e-utils that escapes CSS special characters, and updated all block e2e helpers and test specs to use it.

  • fix(server): Remove unused print mixin from build logger (@lowdefy/server)

    Removed the pino mixin that added a print field to every build log entry. This field was a leftover from a previous CLI display system and caused spurious print: warn lines in build output.

  • feat(e2e-utils): Improved e2e scaffold with new scripts and SLOW_MO support. (@lowdefy/e2e-utils)

    New scaffold scripts:

    • e2e:headed — Run tests with a visible browser in slow motion (SLOW_MO=500, --workers=1)
    • e2e:server — Start the e2e server once, then rerun tests without rebuilding

    SLOW_MO env var:

    • createConfig now reads the SLOW_MO environment variable and passes it to Playwright's launchOptions.slowMo
    • No manual config extension needed — just set SLOW_MO=500 in your npm script

    Scaffold template fixes:

    • Fixed appDir from '../' to './'path.resolve resolves relative to cwd, not the config file
    • Fixed fixtures.js template to use mdbFixtures (plural) from /fixtures subpath with mergeTests
    • Simplified example.spec.js to use /api/auth/session health check — works on auth-protected apps
    • Fixed README template with correct appDir values, "Faster Test Runs" section, and "Common Patterns" section
  • fix(helpers): Prevent makeReplacer from mutating original object marker enumerability. (@lowdefy/helpers)

    makeReplacer used Object.defineProperty to make ~k, ~r, ~l enumerable for JSON.stringify, but operated on the original object reference instead of a copy. This permanently mutated the original, causing internal markers to leak to plugin components via Object.entries/Object.keys.

v4.6.0 Breaking risk
⚠ Upgrade required
  • `~ignoreBuildCheck` has been renamed to `~ignoreBuildChecks`; update config files accordingly.
  • Sentry integration requires setting the environment variable `SENTRY_DSN` if usage is desired.
Breaking changes
  • Renamed `~ignoreBuildCheck` (singular) to `~ignoreBuildChecks` (plural); using the old name throws a migration error.
Notable features
  • Config-aware error tracing shows exact YAML file:line locations with clickable VSCode links.
  • Zero‑config Sentry integration captures client and server errors with Lowdefy context (pageId, blockId).
  • JIT page building in dev server builds pages on demand for faster iteration.
Full changelog

Highlights

Looking at the release notes file and the changelog entries you've provided, here are the developer-friendly bullet points organized by theme:

Error Handling & Debugging

  • Config-aware error tracing shows exact YAML file:line locations with clickable VSCode links in terminal and browser
  • Unified error system in @lowdefy/errors package with standardized TC39 constructor signatures (new MyError(message, { cause, ...options }))
  • Build-time validation provides "Did you mean?" suggestions for typos and catches NEXTAUTH_SECRET configuration issues
  • Plugin errors simplified — operators, actions, and connections throw plain messages; interface layer adds context (received value, location)
  • Sentry integration (zero-config: just set SENTRY_DSN) captures errors on client and server with Lowdefy context (pageId, blockId, config location)
  • New UserError class for expected user-facing errors (validation, intentional throws) — logs to browser console only, never to server terminal

Build & Performance

  • JIT page building for dev server — pages build on-demand when requested instead of all at once, speeding up development
  • Shallow _ref resolution leaves on-demand markers for faster initial builds with file dependency tracking for targeted rebuilds
  • New ~ignoreBuildChecks property suppresses specific build validation errors/warnings, with cascade support to suppress entire page subtrees
  • Build now collects all errors before stopping instead of failing on first error, showing developers all issues at once
  • Schema validation errors stop the build immediately to prevent cascading failures

Developer Experience

  • New @lowdefy/logger package with environment-specific variants (Node, CLI, browser) and standardized logging interface
  • Dev server now accepts LOWDEFY_DEV_USER env var or auth.dev.mockUser config for mock user support in e2e testing
  • Port-in-use check displays clear error message before starting server
  • Build-time validation no longer warns about _state references set by SetState actions

Testing

  • New @lowdefy/e2e-utils package for Playwright e2e testing with locator-first API (ldf.block('id').do.*), request mocking, and state assertions
  • Comprehensive e2e tests for blocks-antd (~700 tests covering 63 blocks) and blocks-basic (~40 tests)
  • npx @lowdefy/e2e-utils scaffold command sets up e2e testing in your project

Bug Fixes

  • Fixed env vars not being passed to Next.js build subprocess (NEXT_TELEMETRY_DISABLED was being ignored)
  • Array line number metadata (~l) now preserved through serializer.copy() — schema validation errors show correct line numbers
  • Error cause chains properly preserved across plugin boundaries and CLI
  • Input block onChange events now pass the input value

What's New

feat: Config-aware error tracing and Sentry integration

Packages: @lowdefy/api, @lowdefy/build, lowdefy, @lowdefy/client, @lowdefy/engine, @lowdefy/operators, @lowdefy/actions-core, @lowdefy/blocks-basic, @lowdefy/connection-axios-http, @lowdefy/connection-knex, @lowdefy/connection-redis, @lowdefy/connection-sendgrid, @lowdefy/operators-change-case, @lowdefy/operators-diff, @lowdefy/operators-js, @lowdefy/operators-jsonata, @lowdefy/operators-moment, @lowdefy/operators-mql, @lowdefy/operators-nunjucks, @lowdefy/operators-uuid, @lowdefy/operators-yaml, @lowdefy/server, @lowdefy/server, @lowdefy/server-dev, @lowdefy/server-dev, @lowdefy/block-utils, @lowdefy/errors, @lowdefy/helpers, @lowdefy/node-utils

Config-Aware Error Tracing (#1940)

  • Errors now trace back to exact YAML config locations with file:line
  • Clickable VSCode links in terminal and browser
  • Build-time validation catches typos with "Did you mean?" suggestions
  • Service vs Config error classification

Plugin Error Refactoring

  • Operators throw simple error messages without formatting
  • Parsers (WebParser, ServerParser, BuildParser) format errors with received value and location
  • Removed redundant "Operator Error:" prefix from error messages
  • Consistent error format: "{message} Received: {params} at {location}."
  • Actions and connections also simplified: removed inline received from error messages (interface layer adds it)
  • Connection plugins (axios-http, knex, redis, sendgrid) no longer expose raw response data in errors

Error Class Hierarchy

  • Unified error system in @lowdefy/errors with all error classes
    • @lowdefy/errors/build - Build-time classes with sync location resolution
  • Error classes: LowdefyError, ConfigError, ConfigWarning, PluginError, ServiceError
  • ConfigWarning supports prodError flag to throw in production builds
  • ServiceError.isServiceError() detects network/timeout/5xx errors
  • ~ignoreBuildChecks cascades through descendants to suppress warnings/errors

Build Error Collection

  • Errors collected in context.errors[] instead of throwing immediately
  • tryBuildStep() wrapper catches and collects errors from build steps
  • All errors logged together before summary message for proper ordering

Sentry Integration (#1945)

  • Zero-config Sentry support - just set SENTRY_DSN
  • Client and server error capture with Lowdefy context (pageId, blockId, config location)
  • Configurable sampling rates, session replay, user feedback
  • Graceful no-op when DSN not set

feat(server-dev): Add mock user support for e2e testing

Packages: @lowdefy/api, @lowdefy/build, @lowdefy/server-dev

Set LOWDEFY_DEV_USER env var or auth.dev.mockUser in config to bypass login in dev server.

Collect all build errors before stopping

Packages: @lowdefy/build, @lowdefy/build, @lowdefy/helpers

feat: JIT page building for dev server

Packages: @lowdefy/build, @lowdefy/operators-js, @lowdefy/server, @lowdefy/server-dev

Shallow Refs and JIT Build (@lowdefy/build)

  • Shallow _ref resolution stops at configured JSON paths, leaving ~shallow markers for on-demand resolution
  • shallowBuild produces a page registry with dependency tracking instead of fully built pages
  • buildPageJit fully resolves a single page on demand using the shallow build output
  • File dependency map tracks which config files affect which pages for targeted rebuilds
  • Build package reorganized: jit/ folder for dev-server-only files, full/ folder for production-only files

JIT Page Building (@lowdefy/server-dev)

  • Pages are built on-demand when requested instead of all at once during initial build
  • Page cache with file-watcher invalidation for fast rebuilds
  • /api/page/[pageId] endpoint triggers JIT build if page not cached
  • /api/js/[env] endpoint serves operator JS maps
  • Build error page component displays errors inline in the browser

Operator JS Hash Check (@lowdefy/operators-js)

  • Added hash validation for jsMap to detect stale operator definitions

Add build-time validation for NEXTAUTH_SECRET environment variable when auth providers are configured

Packages: @lowdefy/build

feat(build): Add ~ignoreBuildChecks property to suppress build validation

Packages: @lowdefy/build, @lowdefy/docs

Build Validation Suppression (#1949, #1963)

  • New ~ignoreBuildChecks property suppresses build-time validation errors and warnings
  • Supports true (suppress all) or array of specific check slugs (e.g., ['state-refs', 'types'])
  • Cascades to all descendant config objects - set on a page to suppress for all child blocks
  • Silent suppression - no log output when validation is skipped (visible with --log-level debug)

Renamed: Previously ~ignoreBuildCheck (singular) - using the old name throws a helpful migration error.

Available Check Slugs:

  • state-refs, payload-refs, step-refs - Reference validation warnings
  • link-refs, request-refs, connection-refs - Action reference validation
  • types - All type validation (blocks, operators, actions, etc.)
  • schema - JSON schema validation errors

Use Cases:

  • Dynamic state references created at runtime by custom blocks
  • Multi-app monorepos with conditional configurations
  • Work-in-progress features during development
  • Plugin development with custom types not yet registered

Example:

# Suppress all checks for this page and descendants
pages:
  - id: dynamic-page
    type: Box
    ~ignoreBuildChecks: true
    blocks:
      - id: block1
        type: TextInput
        properties:
          value:
            _state: dynamicField # No warning

# Suppress only specific checks
blocks:
  - id: custom_block
    type: CustomBlock
    ~ignoreBuildChecks:
      - state-refs
      - types
    properties:
      onClick:
        _state: dynamicState # No warning (state-refs suppressed)

Add e2e testing package for Lowdefy apps

Packages: lowdefy, @lowdefy/client, @lowdefy/blocks-antd, @lowdefy/block-dev-e2e, @lowdefy/e2e-utils

@lowdefy/e2e-utils (new package)

  • Locator-first API via ldf Playwright fixture: ldf.block('id').do.*, ldf.block('id').expect.*
  • Request mocking with static YAML files (mocks.yaml) and inline per-test overrides
  • Request assertion API: ldf.request('id').expect.toFinish(), .toHaveResponse(), .toHavePayload()
  • State and URL assertions: ldf.state('key').expect.toBe(), ldf.url().expect.toBe()
  • Manifest generation from build artifacts for block type resolution and helper loading
  • createConfig() and createMultiAppConfig() for Playwright config with automatic build/server management
  • Scaffold command (npx @lowdefy/e2e-utils) for project setup with templates and dependency management
  • Block helper factory with auto-provided expect methods (visible, hidden, disabled, validation)

@lowdefy/cli

  • Add --server option to lowdefy build for server variant selection (e.g., --server e2e)

@lowdefy/client

  • Expose window.lowdefy when stage="e2e" for e2e state/validation access

@lowdefy/blocks-antd

  • Flatten e2e helper APIs for polymorphic proxy compatibility
  • Add TextArea e2e helper

@lowdefy/block-dev-e2e

  • Remove unused srcDir variable

feat(logger): Add centralized @lowdefy/logger package and standardize logging

Packages: lowdefy, @lowdefy/server-dev, @lowdefy/logger

New @lowdefy/logger Package

  • Centralized logging with environment-specific subpaths: /node, /cli, /browser
  • createNodeLogger — pino factory with custom error serializer preserving Lowdefy error metadata (source, configKey, isServiceError)
  • createCliLogger — wraps createPrint (ora spinners, colored output) with standard logger interface
  • createBrowserLogger — maps to console.* with error formatting
  • wrapErrorLogger — formats Lowdefy errors, emits source as separate { print: 'link' } line for blue clickable links

Standardized .ui Interface

All logger variants expose logger.ui with consistent methods: log, dim, info, warn, error, debug, link, spin, succeed. This allows any component to emit structured output without knowing the runtime environment.

  • dim renders as dimmed text in the CLI — useful for low-priority trace lines (e.g., request logs) that shouldn't compete visually with build output

CLI Logger Migration

  • CLI now uses createCliLogger instead of raw createPrint
  • context.print replaced with context.logger / context.logger.ui
  • createPrint and createStdOutLineHandler moved from CLI to @lowdefy/logger/cli

Server-Dev stdio:inherit

  • Server process spawned with stdio: ['ignore', 'inherit', 'pipe']
  • Server pino JSON flows directly to manager stdout (inherited by CLI) — eliminates dev stdout line handler
  • Only stderr piped for error formatting through manager logger
  • Server createLogger includes print mixin so CLI can render each line correctly

feat(errors): Add UserError class and thread actionId through request pipeline

Packages: @lowdefy/engine, @lowdefy/actions-core, @lowdefy/connection-test, @lowdefy/errors

UserError Class

  • New UserError in @lowdefy/errors for expected user-facing errors (validation failures, intentional throws)
  • UserError logs to browser console only — never sent to the server terminal
  • Throw action now throws UserError instead of custom ThrowActionError

Engine Error Routing

  • Actions.logActionError() routes errors by type: UserErrorconsole.error(), all others → logError() (terminal)
  • Deduplication by error message + action ID prevents repeated logging

actionId Threading

  • actionId threaded from callAction through createRequest to Requests.callRequests
  • Server-dev request handler logs request trace via logger.ui.dim() for dimmed output
  • Enables request logs to include the triggering action for better debugging context

test(blocks): Add comprehensive Playwright e2e tests for blocks-antd and blocks-basic

Packages: @lowdefy/blocks-antd, @lowdefy/blocks-basic, @lowdefy/block-dev-e2e

@lowdefy/block-dev-e2e (new package)

  • Shared test utilities for block e2e testing in the monorepo
  • createPlaywrightConfig for consistent Playwright setup
  • getBlock helper using framework wrapper ID pattern (#bl-{blockId})
  • navigateToTestPage for test page navigation

@lowdefy/blocks-antd

  • ~700 e2e tests covering all 63 blocks
  • Test coverage for input, display, layout, navigation, and overlay blocks
  • Block-specific e2e helpers (Button, TextInput, Selector)

@lowdefy/blocks-basic

  • ~40 e2e tests covering core blocks (Box, Span, Anchor, Html, etc.)

Fixes & Improvements

  • refactor: Consolidate error classes into @lowdefy/errors package with environment-specific subpaths (@lowdefy/api, @lowdefy/build, @lowdefy/client, @lowdefy/engine, @lowdefy/operators, @lowdefy/errors, @lowdefy/helpers, @lowdefy/logger)

    Error Package Restructure

    • New @lowdefy/errors package with all error classes (ConfigError, PluginError, ServiceError, UserError, LowdefyInternalError, ConfigWarning)
      • @lowdefy/errors/build - Build-time errors with sync resolution via keyMap/refMap
    • Moved ConfigMessage, resolveConfigLocation from node-utils to errors/build

    TC39 Standard Constructor Signatures

    • All error constructors standardized to new MyError(message, { cause, ...options }):
      new ConfigError('Property must be a string.', { configKey });
      new OperatorError(e.message, { cause: e, typeName: '_if', received: params });
      new ServiceError(undefined, { cause: error, service: 'MongoDB', configKey });
      
    • Plugins throw simple errors without knowing about configKey
    • Interface layer adds configKey before re-throwing

    configKey Added to ALL Errors

    • Interface layer now adds configKey to ALL error types (not just PluginError):
      • ConfigError: adds configKey if not present, re-throws
      • ServiceError: created via new ServiceError(undefined, { cause: error, service, configKey })
      • Plain Error: wraps in PluginError with configKey
    • Helps developers trace any error back to its config source, including service/network errors

    Cause Chain Support

    • All error classes use TC39 error.cause instead of custom stack copying
    • CLI logger walks cause chain displaying Caused by: lines
    • extractErrorProps recursively serializes Error causes for pino JSON logs
    • ConfigError and PluginError extract received and configKey from cause:
      new ConfigError(undefined, { cause: plainError }); // extracts cause.received and cause.configKey
      new PluginError(undefined, { cause: plainError }); // same extraction
      

    Error Display

    • errorToDisplayString() formats errors for display, appending Received: <JSON> when error.received is defined
    • rawMessage stores the original unformatted message on PluginError
  • fix(build): Eliminate false positive warnings for _state references set by SetState actions (@lowdefy/build)

    The validateStateReferences validator now recognizes state keys initialized by SetState actions in page or block events, eliminating false positive warnings when _state references legitimate state that's set programmatically rather than from input blocks.

  • Improve build error handling and test infrastructure: (@lowdefy/build, @lowdefy/engine)

    • Stop build after schema validation errors to prevent cascading failures
    • Convert makeId to class with reset() method for reliable test isolation
    • Add parseTestYaml helper for realistic YAML-based test fixtures
    • Simplify buildConnections by removing duplicate validations handled by schema
    • Fix addKeys to not store undefined values in keyMap
    • Menu link to missing page is warning in dev, error in prod
    • Handle areas with no blocks gracefully - render as empty page instead of crashing
    • Filter out anyOf/oneOf cascade errors in schema validation - only show the specific error
  • fix(helpers): Preserve ~l line numbers on arrays in serializer.copy (@lowdefy/build, @lowdefy/helpers)

    Fixed an issue where line number metadata (~l) on arrays was lost during serializer.copy(), causing schema validation errors to show incorrect line numbers.

    Problem:

    • Schema errors for properties like requests: at line 7 were showing :1 instead of :7
    • The ~l property on arrays was stripped during JSON round-trip in evaluateBuildOperators

    Solution:

    • Arrays with ~l are now wrapped in a marker object { '~arr': [...], '~l': N } during serialization
    • The reviver restores the array with ~l preserved as a non-enumerable property
    • Custom revivers now receive the restored array instead of the wrapper object

    Result:

    Before: lowdefy.yaml:1 at root
    After:  lowdefy.yaml:7 at root
    
  • fix(errors): Preserve error cause chains in catch-and-rethrow blocks across plugins and CLI (lowdefy, @lowdefy/connection-axios-http, @lowdefy/connection-sendgrid, @lowdefy/operators-js, @lowdefy/operators-jsonata, @lowdefy/operators-nunjucks)

  • Fix env vars not being passed to Next.js build subprocess. The env object was passed as a separate parameter to spawnProcess instead of inside processOptions, so NEXT_TELEMETRY_DISABLED was silently ignored during next build. (lowdefy)

  • Add port-in-use check with clear error message before starting server.

  • fix(errors): Remove redundant try/catch in operator runners, add cause chains to remaining throws (@lowdefy/actions-core, @lowdefy/server-dev, @lowdefy/block-dev)

  • Pass value of inputs to onChange event. (@lowdefy/blocks-antd, @lowdefy/blocks-color-selectors)

Beta — feedback welcome: [email protected]