0.471 (07 Aug 2026)

7 August, 2026

This release lets a flow stop itself from running more than once at a time, adds structured details to notification emails, and ships fixes across list actions, authentication, and generated clients.

Prevent concurrent flow runs

Add the @preventConcurrency attribute to a flow to allow only one active run at a time. While a run is in progress, any trigger that would start a second run is rejected, whether it comes from a schedule, a manual start, a task, a signed link, or a handoff from another flow.

flow SyncLedger {
    @preventConcurrency
    @permission(roles: [Admin])
}

This is useful for flows that reconcile shared state, such as a data sync or a nightly batch, where two overlapping runs would double-count or corrupt data. A blocked trigger gets a 409 response with the message flow already has an active run. See Writing flows for details.

Structured details in notification emails

Notification emails sent with notify.email can now carry an ordered list of key/value details between the body and the action buttons. Pass them as details on the structured email content:

await notify.email({
  recipients: { to: { emails: ["ops@example.com"] } },
  subject: "Order shipped",
  content: {
    title: "Order #1024 is on its way",
    body: "Your order has shipped and is expected within three days.",
    details: [
      { label: "Tracking number", value: "1Z999AA10123456784" },
      { label: "Carrier", value: "UPS" },
      { label: "Delivery estimate", value: "12 August, 2026" },
    ],
    actions: [{ label: "Track order", url: "https://example.com/track/1024" }],
  },
});

Details render as aligned rows in the HTML email and as readable lines in the plain-text version. The plain-text alternative is now stored alongside the HTML, so clients that prefer plain text get a clean fallback without the email being re-rendered. On narrow screens the detail rows stack and the action buttons become full width.

Fixes and Improvements

  • List actions: When a @searchable list action receives a search term, results are now ordered by match relevance ahead of the action's default @orderBy. Exact matches rank first, then text relevance, with the schema ordering kept only as a tiebreaker. An orderBy sent by the caller still takes precedence, and an action that filters across a to-many relation keeps its previous ordering so it does not return duplicate rows.
  • Authentication: Users are now resolved by email case-insensitively when signing in or provisioning through OIDC. Previously a difference in casing, for example Jane.Doe@example.com against jane.doe@example.com, could create a second disconnected user record and leave someone on stale team memberships. Sign-in now converges on a single record. Existing duplicate records are left unchanged.
  • Generated clients: Browser API clients now include only the types reachable from the API's selected actions. Subscriber event types and models excluded from a named API no longer leak into the generated client, which previously could produce client code that failed to type-check.

For any issues or feedback, please contact us at help@keel.so.

Thank you for using Keel!