Release notes

Verified against Flow 5.0.0-beta.39

View as Markdown
On this page

Every change in each Boomerang Flow release, newest first, as the product's changelog records it. For the story of what changed and why it matters, see What's new.

Under Upgrading, an item marked Breaking needs an action from you when you upgrade, and links to the upgrade steps.

5.0.0

Note

5.0.0 isn't released yet. These notes are from 5.0.0-beta.39, and can change before the release.

Upgrading

  • Breaking: see Upgrade from v3. Clearing flow.security.bootstrap-token revokes the bootstrap token. Before, a blank variable left the stored token valid; now the next start deletes it (logged at info), as for a removed declared token. Every instance must carry the same flow.security.tokens entries, because each revokes the declared tokens its own configuration does not name.
  • Users of type auditor and author are plain users. They were counted as admins for the navigation and for creating a workspace when users may not, though they never held the global grant; only admin and operator are admins. Stored users are not changed, and the webapp shows both as User.
  • Breaking: see Upgrade from v3. The dispatcher takes service-core's address as one URL. flow.engine.service.host is gone: set flow.engine.url (FLOW_ENGINE_URL, for example http://service-core:7700). Its default is http://localhost:7700, service-core's port; the old default pointed at 7701, where nothing listens.
  • Breaking: see Upgrade from v3. Catalogue tasks without an image of their own run on boomerangio/task-flow:3.3.0 (the task settings' default.image, was 3.1.0), as do the v3 revisions the upgrade moves off worker-flow. Change unit _0036__DefaultTaskImageTaskFlow33 moves an install whose setting still holds 3.1.0; an image an operator chose is kept. No seeded revision pins a task-flow version any more: _0043__UnpinTaskFlowImages removes the pin from every revision naming boomerangio/task-flow, so each follows the setting (the AI, Box and Python worker images stay). What changes for those tasks:
    • Script tasks run on Alpine 3.24 with Node 24 (was 20), npm 11 at /usr/local/bin/npm (/usr/bin/npm is gone), GNU tar, OpenSSL 3.5, curl 8.23 and bash 5.3.
    • Tasks that exited successfully having done nothing now do their work, or fail visibly: Append Data (Sheets) appends its rows, Copy File (Drive) copies, and the Artifactory download and upload tasks run at all. The download needs a destination path, relative paths resolve against /workspace, Basic auth is sent encoded, and the upload verifies TLS, so a self-signed Artifactory fails until its CA is trusted.
    • The Drive tasks reach shared drives, and List Files honours its corpora and drive ID.
    • The mail, ServiceNow and Slack tasks work when HTTP_PROXY is set, and JSONPath To Parameter no longer crashes with module is not defined.
    • Task logs no longer print param values, and Google errors no longer include the request's bearer token.
  • Breaking: see Upgrade from v3. The GitHub tasks need GitHub App permissions. The GitHub catalogue tasks act with the GitHub App's installation token when a workspace shares it; between them they need Issues: write, Pull requests: write, Contents: read or write, Commit statuses: write and Actions: write on the App. Each task's description names its own, and an organisation approves the App again when its permissions change. The Teams and SharePoint tasks default to the Microsoft integration's parameters, and the new Google Drive tasks to the Google integration's; in a workspace that has not connected the integration, enter the credentials in the task instead.
  • Breaking: see Upgrade from v3. The Java images have no shell. flow-service-core and flow-service-dispatcher run on Ubuntu's chiseled JRE: no shell, package manager or keytool. Set JVM flags in JDK_JAVA_OPTIONS (the JAVA_OPTS variable and the sh -c java … command the v3 chart used no longer apply), and trust an internal CA by mounting a truststore and naming it there (-Djavax.net.ssl.trustStore=…) rather than importing a certificate at container start. The container runs as uid 10001 in group 0.
  • Breaking: see Upgrade from v3. Migrations run inside service-core as it starts. The flow-service-loader image and its pre-deploy Job are gone; remove them from the deployment and give service-core a startup probe that covers the migration. One product tag builds three images.
  • You can upgrade from v3, although runs are dropped.
  • Breaking: see Upgrade from v3. The v3 install must come from bmrg-flow 4.13.5 or later (flow-loader 0.5.9 or later, whose changelog holds changeset 111). An older v3 install is refused at startup: move it to that chart first.
  • Breaking: see Upgrade from v3. Encryption keys are two pairs. mongo.encrypt.secret/mongo.encrypt.salt now mean the v3 pair, read once to decrypt v3's secured settings during the upgrade. flow.encrypt.secret/flow.encrypt.salt is the key v5 encrypts with and is required on every install. Set both before the first start against a v3 database. Secured settings are encrypted with AES-GCM; v3's values are re-encrypted on the first start.
  • Breaking: see Upgrade from v3. Workflow storage is removed. A workflow has one shared workspace per run, declared as a single workspace (size, mount path, class, access mode) instead of a workspaces list of two types; the volume keyed by the workflow is gone. A v3 workflow that had Workflow storage is marked on upgrade and refused at run with WORKFLOW_STORAGE_REMOVED until it is saved again. To keep files between runs, add a Download Artifact task with Scope: Workflow and Optional at the start of the workflow and an Upload Artifact task with Scope: Workflow at the end, both with the same name and the path you mounted before; kept artifacts appear under Configure › Artifacts. Upgraded workflows keep their mount path (/workspace/workflowrun when none was set); new workflows mount at /workspace. Flow no longer manages the old Workflow storage volumes: copy out anything you need, then delete them with kubectl delete pvc -n <namespace> -l boomerang.io/workspace-type=workflow.
  • One storage quota. max.workspace.storage (the run workspace size cap, carried from max.workflowrun.storage) replaces max.workflow.storage and max.workflowrun.storage in the quotas settings group; the per-workspace quota field is maxWorkspaceStorage. Kept artifacts count toward max.artifact.storage until replaced or deleted. The dispatcher's kube.workflow.storage.* properties are gone.
  • Breaking: see Upgrade from v3. The CloudEvent trigger works, with a new run shape. POST /api/v2/event used to fail before a run was saved. A run now gets event, the event's attributes, and data, its data, and no longer one param per top-level data member: read $(params.data.<field>). The trigger's filters read the event as sent (type, data.tag). ref still takes a workflow id; add workspace to name the workflow instead (?workspace=<workspace>&ref=<workflow name>, on POST /api/v2/webhook too), which is the URL the editor now shows. Without ref, the subject's first segment names the workflow. v3 workflows that filled properties from a CloudEvent's data with a jsonPath need changing: see "Upgrading from v3" in the README.
  • Breaking: see Upgrade from v3. Before a v3 upgrade, check for users sharing one email. The migration never deletes an account, so a shared email stops startup until it is resolved. See "Upgrading from v3" in the README.
  • Breaking: see Upgrade from v3. flow.mode is gone; two properties express the deployment shape. flow.security.enabled (default true) switches authentication and authorization together, and flow.workspace.single (default false) serves the seeded system workspace alone. An embedded, headless engine sets false and true. Remove FLOW_MODE from every deployment. A secured install with no person to sign in takes its first credential from flow.security.bootstrap-token, registered as a global machine token at start.
  • Breaking: see Upgrade from v3. Finished runs are now purged after the Activity retention, 365 days by default. The new "Activity Retention (days)" setting in the workflow-run group (floored at 60) deletes each finished run older than it, with its task runs, actions and run-scoped artifacts and logs, once an hour. A run that a retry or a child run still points at is kept until that run goes. Workflow-scoped artifacts and audit events are never touched. The purge is on from the first start: its first pass runs within a minute and each hourly pass removes up to 1000 runs, so an upgraded install loses its runs older than a year within hours, before the setting can be changed, and no value keeps every run. This is by design: export any history you need before upgrading (README, "Upgrading from v3").
  • Breaking: see Upgrade from v3. The seeded ai task is removed; the AI tasks are ai-prompt and ai-agent on boomerangio/task-ai:2.0.0. An upgraded database keeps any ai definition it already holds, so its workflows still resolve, but nothing maintains it: move nodes to ai-prompt, whose param names are the same. temperature and maxTokens now have no default and are sent only when set. On service-dispatcher, flow.dispatcher.ai.image is gone: every ai revision declares its image, which must start with one of flow.dispatcher.ai.image-prefixes (default boomerangio/task-ai) or the task is refused with ImageNotAllowed. flow.dispatcher.ai.endpoint and flow.dispatcher.ai.token (blank) reach every ai pod as OPENAI_BASE_URL and OPENAI_API_KEY.
  • flow.uniquenames.enabled is gone. A task run's taskRef on the API, the status events and the dispatcher protocol is the task's slug form, <slug> for a global task and <workspace>/<slug> for a workspace task, as workflow nodes already showed; storage keeps the id, so nothing is migrated. A dispatcher's task poll filter takes the same form: a bare slug now matches only a global task.
  • A workspace quota of 0 is unlimited, and an absent quota inherits the platform default. A workspace PATCH merges quotas field by field instead of replacing the stored set.
  • Breaking: see Upgrade from v3. Settings and global parameters are updated by PATCH; their PUT routes are gone. Send a JSON Merge Patch (application/merge-patch+json or application/json): PATCH /api/v2/settings with {"<group>": {"<key>": value}}, for example {"artifacts": {"retention.default.days": "30"}}, and PATCH /api/v2/parameters/{name} with only the members to change. An absent member is unchanged, null clears it, and a secured value sent as ***** is kept. An unknown settings group or key is now a 400 SETTINGS_INVALID_KEY rather than ignored.
  • Breaking: see Upgrade from v3. One rule for secret values on every layer, and blank now clears. A set secret reads ***** and an unset one reads null on parameters, workflow defaults and node values, task defaults, schedules, settings and runs (workflow defaults used to read blank; node, schedule and run values used to read ***** even when unset). On a write, *****, null or a missing value keeps the stored secret, "" clears it, and any other value sets it. Before, a blank kept the secret: a client that sends "" to mean "unchanged" must send ***** or leave the value out. On a run request, a param sent as null or *****, or left out, keeps the declared default for every param type. A value that is only a reference, such as $(workspace.params.token), reads as written.
  • A workflow body's unknown top-level fields are ignored and no longer echoed back in the save response, so the response shows exactly what was stored.
  • The workflow canvas read masks secrets like every other workflow read. A Slack integration's tokens are now encrypted at rest; one stored before this release keeps its plaintext token until Slack is next linked, with no migration.
  • Breaking: see Upgrade from v3. A dispatcher must heartbeat every task it holds. A claim now carries a lease from the moment it is made, 90 s ahead (flow.dispatcher.lease-ms), and each heartbeat renews it, so a claim its dispatcher stops reporting is requeued even when the dispatcher restarts under the same id. Flow's Kubernetes dispatcher and the dispatcher SDK heartbeat every 30 s from the claim; a dispatcher written against the protocol directly that sends no heartbeat has every claim requeued 90 s after it was made.
  • Breaking: see Upgrade from v3. The inbound Slack routes require Slack's request signature. Set the Slack app's signing secret in the integration settings (slack.signingSecret); with none set, the commands, interactivity and events routes refuse every request.
  • Breaking: see Upgrade from v3. Every GitHub App delivery requires GitHub's signature, and the GitHub trigger now works. Set the App's webhook secret in the integration settings (github.webhookSecret); with none set, every GitHub event is refused with 401, where before only installation events were. A GitHub event now starts the workflows whose GitHub trigger is on (it used to read the webhook trigger's switch), its Events and Repository filters apply, and one workflow refusing no longer stops the rest of the workspace. The run gains an event param, {type, id}, beside data.
  • The Slack tasks take their bot token and webhook URL as password fields. Change unit _0030__SeedSlackCredentialRevisions adds a new revision of each of the 13 Slack tasks, so a token typed into a node is stored encrypted and read back as *****. Workflows keep the revision they pin until upgraded in the editor; upgrading keeps the value, which is encrypted on the next save.
  • Breaking: see Upgrade from v3. The GitHub integration can share its installation token with tasks. Once GitHub is connected, the workspace's integration page offers "Share the installation token with tasks", which writes two read-only workspace parameters, github-token (refreshed every 45 minutes) and github-url; workspace parameters must be on. Change unit _0031__SeedGitHubIntegrationParameters gives the GitHub integration template those fields and adds a revision of each of the 17 GitHub tasks whose token and url default to them; a token typed into a node still wins. A task upgraded to the new revision and left on its default fails at admission in a workspace that does not share the token; the editor warns at save. A read-only workspace parameter an integration manages can no longer be changed or deleted through the workspace (409 PARAMS_MANAGED_BY_INTEGRATION), and readOnly sent on a workspace update is ignored.
  • Breaking: see Upgrade from v3. Slack is a workspace integration and can share its bot token with tasks. The Integrations page now lists Slack once slack.clientId and slack.clientSecret are set: Connect installs the Flow Slack app and links that Slack team to the workspace, and "Share the installation token with tasks" writes a read-only slack-token workspace parameter. In the Slack app's settings, set the OAuth redirect URL to <Flow API URL>/api/v2/integration/slack/auth, add the bot scopes channels:manage, channels:read, chat:write, commands, files:write, groups:read, groups:write, users:read and users:read.email, and leave token rotation off, since a rotating token cannot be shared. The slack.installURL setting is no longer read: GET /api/v2/integration/slack/install builds Slack's authorize URL itself. Change unit _0032__SeedSlackIntegrationParameters makes the Slack template active and adds a revision of each of the 10 Slack tasks that call the Slack API whose token defaults to $(workspace.params.slack-token). The share route is now POST /api/v2/integration/{github|slack}/share; the GitHub URL is unchanged.
  • Google and Microsoft can be connected by entering their credentials. The Integrations page lists them with no platform setting needed: Connect asks for a Google Cloud service account's JSON key, or a Microsoft Entra app's tenant ID, client ID and client secret, and writes them as read-only workspace parameters (google-service-account-key; microsoft-tenant-id, microsoft-client-id, microsoft-client-secret). Manage edits them, a stored secret kept until it is retyped, and Disconnect removes them. The share route takes them as values. Change unit _0034__SeedFormIntegrations adds the two templates and marks GitHub and Slack as integrations Flow installs. No catalogue task defaults to them yet.
  • Breaking: see Upgrade from v3. The last "team" names on the API say "workspace". GET /api/v2/profile returns the user's workspaces as workspaces (was teams), and the error reasons TEAM_INVALID_REF, TEAM_INVALID_REQ, TEAM_NON_UNIQUE_NAME, TEAM_INVALID_USER_ROLE and WORKFLOW_TEAM_INACTIVE are now WORKSPACE_INVALID_REF, WORKSPACE_INVALID_REQ, WORKSPACE_NON_UNIQUE_NAME, WORKSPACE_INVALID_USER_ROLE and WORKFLOW_WORKSPACE_INACTIVE, with the same codes. A client that matched a reason or read teams moves with them.

Added

  • Admin › Tokens sets a token's actor. The create form for a platform (global) token chooses Service (the default, a system, script or dispatcher), Agent (an AI agent) or None (a person's token), so a token made there can run the dispatcher protocol, which needs one. The Actor column names the kind. POST /api/v2/token now refuses an actor that does not fit the token (TOKEN_INVALID_ACTOR, 400): a global token takes none, SERVICE or AGENT, a user token none.
  • Machine tokens declared in configuration. flow.security.tokens.<name>.* registers a global token at start, named by the key: FLOW_SECURITY_TOKENS_DISPATCHER_HASH, _PERMISSIONS=workspace/read and _ACTORKIND declare dispatcher. A name is lowercase letters and digits only (an underscore in a variable name becomes a dot), and bootstrap is not usable. Give a hash (the SHA-256 of the raw token, so the environment never holds it) or a value, optional permissions such as workspace/read (none for a dispatcher-only token), and optionally actor-kind (default SERVICE) and expiry. Removing an entry revokes its token, and two entries are valid at once so a token rotates without a gap. A declared token cannot be deleted through DELETE /api/v2/token/{id}: it answers 409. With none declared nothing changes.
  • Sign-in with OpenID Connect can be configured at install time. flow.auth.oidc.issuer and flow.auth.oidc.client-id (FLOW_AUTH_OIDC_ISSUER, FLOW_AUTH_OIDC_CLIENT_ID) fill the auth settings' oidc.issuer and oidc.clientId at startup, only while they are empty; a value set in Settings is never overwritten, and a later change to the property does not replace it. The issuer must be an absolute https URL (http only for localhost), or the start fails. Without them sign-in stays in proxy mode.
  • CloudEvents sink filters. flow.events.sink.types, statuses (any run status, or terminal) and phases narrow what is sent, and each destination takes the same keys plus labels (key=value). A transition no sink wants is never stored, so a filtered event is not counted as dead; labels filter at delivery. phases=completed is the true end of a run with finally tasks. With no filter keys nothing changes.
  • A run's locks go with the run. A run that completes with any status, or is purged, holding an acquirelock lock it never released frees it at once, so the next acquirelock on the key succeeds on its first attempt instead of waiting out the lease; a lock another run holds is never touched.
  • Finally tasks. A task linked from End with the execution condition finally runs once the run has ended, any way: succeeded, failed, cancelled or timed out. The run takes its final status when its graph ends and stays in the running phase while its finally tasks run in parallel, each with its own timeout under the workflowrun setting finally.timeout (5 minutes), an operator's ceiling outside the run's own timeout that no workflow can raise; the stage counts in the run's duration and holds its concurrency slots; the run completes when the last of them ends, and a finally task that fails turns a succeeded run into failed with the reason FinallyFailed. A finally task reads the run's outcome as $(context.params.workflowrun-status) and $(context.params.workflowrun-status-reason); a cancel during the finally tasks does nothing more, a purge still stops them. Finally tasks never chain: saving refuses a finally dependency on anything but End and any dependency on a finally task (400 WORKFLOW_INVALID_FINALLY). In the editor End gains an output port; a task linked from it shows a Finally badge and no output port, and its link a fixed finally condition.
  • A workspace can be found by externalRef, an embedding product's own id for the record it mirrors: GET /api/v2/workspace/query?externalRef= answers the one workspace carrying it, within the caller's access, and a create naming a reference a workspace already carries is 409 WORKSPACE_EXTERNAL_REF_TAKEN with that workspace's name, so a retried create is idempotent. The reference is set once at create and never patched; every reader of the workspace sees it, and the webapp shows it on Settings. A unique sparse index, external_ref, joins the inventory.
  • Templates are workflows of the system workspace. A workflow carries a type: workflow (the default), template or starter, and the last two can be created, versioned and edited only in the system workspace, through its ordinary workflow routes. A workspace runs a template by reference: a workflow created with templateRef: {name, version} shows the template's tasks and params read-only under its own name, labels and triggers, accepts run-time values only for the params the template declares, and each submit runs the template's pinned or latest revision in the workspace, so the run, its quotas and its audit are the workspace's. A template with such workflows cannot be deleted. A starter is copied, as the Home page has always done. Change unit _0038__WorkflowTemplatesIntoSystem moves the seeded templates and a v3 install's own into the system workspace as starters and drops the workflow_templates collection.
  • Templates and starters appear in every workspace through its own workflow routes; there are no /workflowtemplate routes. GET /api/v2/workspace/{workspace}/workflow/query lists, beside the workspace's workflows, the templates and starters it has not made its own, each with its type, and types= narrows; the get, compose, changelog, export and available-parameters routes answer a template by name when the workspace has no workflow of that name. The first use makes it the workspace's own: POST …/workflow/{template}/submit creates the link under the template's name and runs it, and PUT …/workflow naming a template creates the link and saves the display name, description, icon, labels and triggers. A template's runworkflow task that names another template runs it the same way, as the run's workspace's link, so a template's children never run in the system workspace on a workspace's behalf. The webapp's Workflows page shows templates beside the workspace's workflows, tagged and read-only, dashed until first use; the editor puts the task palette away for one; Home's starters come from the workspace query with types=starter. The system workspace changes what a workflow is with PATCH /api/v2/workspace/system/workflow/{name} and {type}: the card's overflow menu offers Make a template, Make a starter or Make a workflow, the system list shows its starters and tags both kinds, and its templates stay fully editable there; a template other workspaces link stays one.
  • A run can be purged on request: DELETE /api/v2/workspace/{workspace}/workflowrun/{id} removes the run with its task runs, actions, artifacts and stored logs, and its lineage (the retries that reuse it and the child runs its tasks started); DELETE …/workflowrun?labels=k%3Dv does the same for every run carrying the labels, a page of 50 a call, reporting how many remain. A run in flight is cancelled first; one whose tasks are still ending, or held by a dispatcher awaiting its terminate poll, is removed by a new watcher sweep once they have settled, or after ten minutes. Both need workflowrun/delete (owner and admin) and are audited as destructive. Audit records stay. A new sparse index, purge_sweep, joins the inventory.
  • 16 catalogue tasks from boomerangio/task-flow:3.3.0: GitHub (create an issue, comment on an issue or pull request, create a pull request, set a commit status, dispatch a workflow, create a release, get file contents), Microsoft Teams (send a message, create a channel, add a member), SharePoint (upload, download and list files) and Google Drive (upload, download and share a file). The Google Sheets and Drive tasks gain a revision that takes the whole service-account key file. Change unit _0035__SeedTaskFlowIntegrationTasks adds them to existing installs.
  • The SDK's Runs.purge(id) and Runs.purge(labels) call the two run purge routes and read {purged, scheduled, remaining}.
  • The thin workflow-run status event (flow.events.sink.payload=thin) carries statusReason, as the task event already did, so a consumer acting on a terminal event reads why the run ended without calling back.
  • The dispatcher wire carries the retry story: a task order has attempt, attempts (how the superseded attempts ended, with their dispatcher) and, on a terminate order, statusReason; a workflow run order has statusReason; and the end request takes retryable: false to mark a failure final, which the engine never requeues whatever the node's retry setting. The SDK's TaskFailure.terminal sends it. A task order also carries retry: {max, failures}, the node's budget and how much of it the engine has spent, so a handler that acts differently on a failure Flow will not retry needs no count of its own; the SDK's TaskRun.isLastAttempt() reads it. Additive, protocol version 1.
  • A task node can retry a failure its dispatcher reports: retry: {max, backoff: {type, initial, max}} on the node, edited on the task's Configure tab. The engine requeues the task on that budget with that backoff, never for a cause a retry cannot change (ImageNotAllowed, AdmissionDenied, ResultsTooLarge or an engine-written cause), and records every superseded attempt on the task run as attempts, which the run's task view lists. A task without the setting runs once, as before; the engine's own requeues of an attempt that never ran keep their separate budget.
  • A workflow can limit its runs in flight: concurrency: {limit, overflow} on the workflow, edited under Configure → Execution. A run over the limit is parked as waiting until a run of the workflow ends (queue, the default) or ends at once as cancelled with reason ConcurrencyLimit (skip). The workspace quota keeps precedence. A workflow run now carries statusReason beside statusMessage, as a task run does.
  • Work reaches a dispatcher as soon as it is ready: admitting a task, or submitting a run with a workspace, wakes the dispatcher polls on that engine instance that can claim it, and a cancel, timeout or retry reaches the holding dispatcher within the end round trip, so a hop in a sequential chain is about 100–200 ms. A dispatcher whose slots are all taken claims new work the moment one frees. The timed re-check still covers work admitted on another replica.
  • flow.dispatcher.poll.recheck-ms sets how often a held dispatcher poll looks at the claimable page again (default 1000, floor 100). A lower value shortens every hop of a sequential chain at the cost of more indexed queries per dispatcher: its idle queries per second are its pools × 1000 / the value.
  • The dispatcher SDK captures a task's log and opens its trace for an in-process dispatcher: with captureTaskLogs(new TaskLogCapture()) on the builder, the lines a handler logs (tagged taskRunId in the MDC, the first and last 64 KB kept) are uploaded as the task log when it ends and every 30 s while it runs; with observationRegistry(registry), each handler runs under a flow.task observation carrying the task's traceparent. Both are off by default; a logback appender is included, SLF4J and logback stay optional.
  • Insights group task runs: GET /api/v2/workspace/{workspace}/insights/tasks counts the period's task runs per task, type, workflow or label, optionally per hour, day or month, with mean, p50, p95 and max duration, the tokens and cost of its AI task runs, the wall clock of the runs sharing a label, and the slowest task runs. The Insights page shows it under Task runs, grouped by task unless another grouping, Model and For-each item included, is chosen.
  • Task runs can be queried across runs: GET /api/v2/workspace/{workspace}/taskrun/query pages them by label or run (one of the two is required), workflow, task name, status and date, as the run query pages runs. A row leaves out params, results, spec, annotations and status message; read the run for those.
  • A task run shows attempt once a dispatcher has claimed it: 1, plus one per requeue. The run view shows it when a task was requeued.
  • Server labels: an AI task run carries boomerang.io/model, the model its model param asked for, and every item task run of a for-each, and a child run a for-each run-workflow task started (on the run and its task runs), carries boomerang.io/item, its item. Labels under boomerang.io/ are now reserved for the server, as annotations already were: a client's are dropped on submit, start and retry.
  • The wait-for-event callback (POST/GET /api/v2/callback) takes an optional eventId. A repeat of the same callback for the same run is acknowledged and not applied again for 7 days; without one, each delivery is applied as before.
  • A run's timeout bounds execution only: while every in-flight task waits on an approval, a manual task, an event, a child run, a sleep or a lock, the deadline is parked and moved forward by the time waited, and the same applies after a pause. A wait is indefinite unless its task declares a timeout. A run's reported duration is execution time. Gated workflows no longer need a budget sized for their approvers.
  • A retry can start from the tasks that did not succeed, or from named tasks: PUT …/workflowrun/{id}/retry takes an optional {scope: all | failed | nodes, nodes, params, labels}. The rest is carried over as copies marked reusedFromRef, and the run records retryFrom. The run page's Retry run and each task's Retry from here open the same choice. The new run starts with an empty run workspace, so files the carried-over tasks wrote there are not carried over; pass them as workflow-scoped artifacts, or re-run everything. The dialog says so.
  • secret is a parameter type. Secret values are encrypted at rest under flow.encrypt.*, masked on every read, and never written to a log. The Box tasks' privateKey is one: _0044__BoxPrivateKeyIsASecret types it as a password and seals the value a workflow already holds for it. The placeholder default a few older GitHub revisions and the SendGrid template task carried on a credential parameter is cleared (_0045__ClearPlaceholderDefaults).
  • The audit trail records global parameter, settings, catalogue task and schedule writes, and every write that changes or clears a secret names it in the event's detail, never its value.
  • A run-workflow task may run for each item, one child run per item, and a waited child's results become the task's results.
  • A run-workflow node may carry params its child workflow declares, such as an upstream task's result; the child run receives them resolved. A param the child does not declare is refused at save with WORKFLOW_INVALID_CHILD_PARAM (1218). A child run receives only the params its node passes, plus item and index under for-each; the parent run's params are not handed down, as in v3.
  • Revision 2 of ai-prompt and ai-agent, on boomerangio/task-ai:2.1.0, takes images: images lists run-workspace paths or https URLs, with imageDetail and maxImageBytes (5 MiB). The image also exports OpenTelemetry spans when an OTLP endpoint is configured. Revision 1 stays on 2.0.0 for workflows pinned to it.
  • Every task gets a TRACEPARENT derived from its run and task run ids, so one run's tasks share one trace, and TaskRun.traceparent() in the dispatcher SDK computes the same value. On service-dispatcher, flow.dispatcher.ai.otel.endpoint, .headers and .protocol (blank by default) give ai pods the OTLP exporter settings. Flow stores no traces; point them at Langfuse, Phoenix, LangSmith or Tempo.
  • Insights filter by run labels and roll up AI usage: tokens, cost and a breakdown by model for the period, and tokens and cost per task.
  • A dispatcher may end a task with RateLimited, which requeues it with backoff like ExceededQuota.
  • A task whose Kubernetes API could not be reached, on a create or a watch (a timeout, a refused connection or a 5xx), ends ClusterUnavailable instead of DispatchError, and the engine requeues it with backoff like ExceededQuota; other 4xx answers keep their reasons. Before the next attempt runs the task's Job or TaskRun is removed, so it never runs twice at once.
  • A stopping dispatcher hands back the claims it never started, ending each DispatcherStopping, which the engine requeues at once instead of after the 90 s lease. Dispatcher.stop() in dispatcher-sdk now waits for the poll in progress within its 10 s drain timeout rather than abandoning it; a host that embeds the SDK can set drainTimeout to 30 s or more to cover a poll the engine holds for its full window.
  • A failed task's status message carries the container's exit code and the tail of its log.
  • $(workspace.params.x) replaces $(team.params.x), which still resolves, logs a warning, and is removed in the next major version.
  • dispatcher-sdk adds StatusReason, naming every reason a dispatcher sends; TaskFailure's four reason constants are the same values.
  • The public API has a checked-in OpenAPI contract, contracts/api-v2.yaml, generated from the document service-core serves at /api/docs/spec/api-v2 and held equal to it by a test; an integrator reads the routes and models from the file at the tag they run.
  • flow-sdk, a Java library for the public API: Flow.connect(url, token) then typed calls to submit, start, read, pause, resume, cancel and retry workflow runs, query runs and task runs by label, read a task run's stored log, read, query and decide manual and approval actions, read Insights and probe the server's health, with the server's error carried as FlowApiException. It depends on spring-web and Jackson only, and is published to GitHub Packages by every product tag beside dispatcher-sdk, at the tag's version (flow-sdk/README.md).
  • A for-each parent's task run carries foreach: true in the public TaskRun, and the task-run query takes foreach=true|false to list parents only or leave them out; absent lists both as before. A parent stands for its items in the graph and never runs, so a running or waiting count over a run's task runs can now exclude it without reading the workflow. flow-sdk adds TaskRunQuery.foreach(boolean) and TaskRun.foreach().
  • A features setting, Workspace creation by users (workspaceCreate, on by default), says whether a user who is not an admin may create a workspace; Home and Workspaces offer the button only to those who may.
  • Insights across workspaces for a global grant: GET /api/v2/insights and GET /api/v2/insights/tasks compute the workspace summary and task breakdown over the workspaces named by workspaces, or every workspace, and GET /api/v2/insights/taskruns lists task runs newest first with their workspace, by label. A workspace-scoped grant is refused. The SDK gains Flow.insights() with InsightsQuery.workspaces(...) and taskRuns(...).
  • Invite-only sign-up: the features setting Auto-Create Users (autoCreateUsers, on by default, which is how sign-in has always worked), switched off, refuses a person who is not a user yet, unless an admin has added them to a workspace or the admin claim below makes them an admin. A person added to a workspace by email is activated by their first sign-in (it was refused before). _0046__SeedAutoCreateUsersSetting adds the setting to an upgraded install.
  • Admins from the identity provider: flow.security.admin-claim.name and flow.security.admin-claim.values (FLOW_SECURITY_ADMIN_CLAIM_NAME, FLOW_SECURITY_ADMIN_CLAIM_VALUES, comma-separated) name a claim, a string or an array such as roles or groups, and the values that make its holder an admin. With a claim set, the provider decides admin on every sign-in that carries the token: a holder is made an admin, one who no longer matches is made a user and their sessions end, and the first sign-in on an empty install is not an admin by being first. Unset, nothing changes. The signature-checked id_token of the webapp's sign-in is always read; the unverified JWT an OAuth2 Proxy forwards is read only with flow.security.admin-claim.trust-forwarded-token=true (default false).
  • The first admin of an empty install is made only by the activation code, on every sign-in path. The webapp's sign-in no longer makes its first user an admin: it asks for the code on the activation screen, and a wrong or missing code creates nobody (behind a proxy, PUT /api/v2/activate checks the code before it registers the caller). With an admin claim set, a sign-in whose claim is read (the webapp's, or a trusted forwarded token) is decided by the claim and the code is not used; one that cannot read it (a forwarded email, an untrusted forwarded token) still needs the code on an empty install. The bootstrap token and other machine tokens are not users: they work from first start and never activate the install. Once the install has users the activation endpoint refuses every code.
  • The activation code is FLOW_ACTIVATION_CODE (flow.activation-code), and it has no default. The old FLOW_OTC / flow.otc is still read when the new name is unset and logs a deprecation warning; both set to different values stops the start. The code must be at least 32 characters or the start fails, and with none set nobody can create the first admin and the start warns: set it before a fresh install is first signed in to. The built-in default test is gone. The compose stack and the local Spring profile set a fixed development value; the e2e setup types it. The otc field of PUT /api/v2/activate is unchanged.

Changed

  • The Auto-Create Users description says who can still sign in. With the setting off, a person who is not a user yet signs in only if an admin has added them to a workspace or the admin claim admits them; existing users are unaffected. The text is seeded on a fresh install, and an install that already holds the old one keeps it.
  • Dispatcher registrations expire. A registration whose dispatcher has not connected for 7 days is removed by the watcher, so the replicas a platform replaces on every deploy no longer pile up. A dispatcher that comes back registers again on its next call and is given a new id; the dispatcher SDK and Flow's Kubernetes dispatcher already do.
  • The Google Sheets (revision 4) and Drive (revision 3) tasks default their service-account key to $(workspace.params.google-service-account-key), the Google integration's parameter, as the Drive file tasks do. A workflow moves when it is upgraded in the editor. Upgraded, a node that still signs in with Client Email and Private Key in a workspace without the Google integration fails at admission with no such workspace parameter, and the editor warns at save; type the key into Service Account Key, or connect Google. Where the workspace has the parameter, its key is used ahead of Client Email and Private Key. Change unit _0037__SeedGoogleKeyDefaultRevisions adds the revisions to existing installs.
  • The dispatcher runs the upload and download artifact tasks on boomerangio/task-flow:3.3.0 (was 3.2.1), whose runtime base moved from an end-of-life Alpine 3.20 to 3.24; the artifact commands are unchanged. An operator who pins flow.dispatcher.artifact.image keeps their own value.
  • Outside a workspace (Home, Admin, Profile) the side navigation points at the last workspace opened in this browser, while you can still open it.
  • A dispatcher that cannot provision a run's workspace reports the cause: the run shows it and retries after a minute (flow.watcher.provision-retry-ms, 60000), failing on the third attempt. A dispatcher built on an older SDK keeps the sweep's release.
  • A run a person or a token starts records who started it in initiatedByRef, and reads name them in the new initiatedByName; the run page and Activity's Initiated By column show the name.
  • A run's duration is shown as active time on the run page and in Activity: it excludes waits for approvals, manual tasks and sleeps, and pauses.
  • Home counts each member of your workspaces once (the profile's workspaceMemberCount).
  • The task edit modal keeps its description to one line, carries the task name and the tabs in its header, and lists a task's declared results on a Results tab, so the fields get the height.
  • A run's workspace is its workflow's owner, looked up by id, so a run in flight keeps its workspace's parameters, secret redaction and lock scope when the workspace is renamed. A task run takes the task settings (timeout ceiling, default image, deletion policy) when the run is queued, so a retry, a run migrated from v3 included, takes them as they stand then. A run's only boomerang.io/* annotations are generation and kind; a submit or start that sends one has it dropped.
  • Building from source needs no GitHub token: Maven resolves every dependency from Maven Central. The dispatcher SDK is still published to GitHub Packages.
  • Spring Boot 4.1.1, with Tomcat 11.0.27, Jackson 2.21.8 and 3.1.8 and commons-io 2.22.0 ahead of it, and the latest chiseled JRE base image, for the critical and high CVEs fixed since 4.1.0. The webapp image has no npm or corepack: the server runs on node alone.
  • The Java images start from a JDK AOT cache (JEP 514/515) and run with compact object headers (JEP 519). Each image build starts the application once on the runtime JRE, with no database or cluster, and records the loaded and linked classes; a container start then reads them instead of redoing that work, which takes roughly 40 % off the time to a healthy service-core and service-dispatcher. Compact object headers take 4 bytes off every object on the heap. Both are set in the image's command, under any JDK_JAVA_OPTIONS a deployment adds; an option there that changes the heap layout (another garbage collector, headers off) makes the JVM skip the cache with a warning and start as before. The images ship the application extracted (service.jar plus lib/), which the cache requires, and each is about 120 MB larger for the cache; the health probe and ports are unchanged. The dispatcher handles requests on virtual threads, as service-core already did.
  • The chiseled JRE base image is refreshed to Canonical's current build (OpenJDK 25.0.4.1), which scans clean, and the shipped dependencies with an open advisory are moved past it: nimbus-jose-jwt 10.3, the Slack client 1.45.3 (for its okio 3.4), body-parser 1.20.9, axios 1.20.0 and dompurify 3.4.17.
  • GET /api/v2/context no longer carries platform.privateTeams, which was always false.
  • A run-scheduled-workflow task passes the scheduled run only its node's other params, not its own futureIn, futurePeriod, timezone and time, matching the run-workflow task.
  • The wfe-url context param points to the wait-for-event route, /api/v2/callback.
  • A workspace's monthly run-duration figures (currentRunTotalDuration, currentRunAverageDuration) cover the whole UTC calendar month. They used to stop at midnight before the month's last day, so that day's runs were never counted.
  • A workflow's Insights retriedRuns counts the runs that are a retry of an earlier run, by hand or automatic. It counted the runs whose workflow allowed retries, whether or not any happened.
  • A switch that never ran, because the path into it was not taken or the run ended first, takes none of its branches, so the tasks after it are skipped and the run carries on. It used to fail the run with a null-value error when the next task was queued.
  • An approval task ends with the five results it declares: approvalStatus (approved or rejected), approvalDate (ISO 8601, UTC), approvalUserName, approvalUserEmail and approvalComments, taken from the approver whose action decided it, so $(tasks.<name>.results.approvalStatus) reads a value instead of an empty string. Its "Number of Approvals" setting (numberOfApprovals) is honoured; the engine read a different name, so every approval completed with one approver. numberOfApprovers is still read where a definition sets it. Each approval is kept as it is given, so one that needs several approvers completes when the last of them acts.
  • Each status event describes the transition it was recorded for: its CloudEvent id is the outbox row's id, the same on every delivery of that transition, and time is when the transition occurred. The graph's end node sends no task event; the run's own event reports the end, and a skipped or refused task and a workflow timeout send one event each.
  • A dispatcher call runs as the dispatcher's own token, so code on the dispatcher wire reads its caller the way code on /api/v2 does. An audit record of a run that a dispatcher's end call completes names that token as the actor, where it named system.
  • While a task runs, its log is read live from its dispatcher even when a copy is stored; a copy stored during this attempt answers only when the dispatcher has no live route or its route is unavailable, and once the task ends the stored copy is served first.
  • A followed task log that has written nothing for 10 minutes ends with a line saying the view stopped following, instead of the connection dropping mid-log; open the log again to keep following. No new property: the two-hour async request timeout applies only to the streamed routes, both task logs and artifact downloads.
  • Task logs and artifacts store reliably on S3-compatible stores such as SeaweedFS: the store's client sends checksums only where S3 requires them, so a log is no longer refused now and then with "The Content-Md5 you specified is not valid".
  • Settings are built from the shipped seed on first start, carrying an upgraded install's own values, so an upgraded and a new install have the same settings documents.
  • The same indexes are built on every install; the token lookup index is now built on a new install.
  • Migrated v3 approver groups are reachable from their workspace again.
  • The sign-in routes the webapp's server calls are /internal/v1/auth/{config,exchange,logout}, outside the public /api/v2. Route /api/v2/** from outside and leave /internal/v1/** unrouted.
  • The dispatcher wire is /dispatcher/v1. service-core and service-dispatcher move together under one tag. Route /dispatcher/v1/** to service-core only for a dispatcher that runs outside its network.
  • Task logs are stored by service-core when it has an artifact store. The dispatcher uploads a finished task's log (PUT /dispatcher/v1/taskrun/{id}/log, capped by flow.engine.task.log.max-bytes, 10 MiB) and core serves that copy once the task has ended, so a log outlives the pod and needs no dispatcher up. Otherwise core reads the live log from the dispatcher that holds the claim, at the URL it registered: set flow.dispatcher.logs.url on service-dispatcher (for example http://service-dispatcher:7702/api/v1/logs/stream). Core's flow.agent.service.host and flow.agent.logstream.url are gone: nothing on core names a dispatcher, and a dispatcher that registers no URL has no live log to follow. The dispatcher's live log route, /api/v1/logs/stream, accepts only the bearer it generates at start and registers; nothing but core can read it. Its v3 GET /api/v1/logs read is gone, and it serves no OpenAPI page. Two new keys in the Artifacts settings group govern storage: "Store Task Logs" (on) and "Task Log Retention (days)" (7), apart from artifact retention; stored logs never count toward artifact storage. Set flow.dispatcher.logs.upload=false on a dispatcher to keep its logs in the pod only.
  • List filters behave the same on every route. A labels entry without a value, such as labels=team, is a 400 QUERY_INVALID_FILTERS everywhere instead of a server error on most lists. A label is split once on its first =, so labels=a%3Db%3Dc matches the value b=c (it was read as b). The user list's ids filter now matches. The action list's and action counts' toDate is exclusive, like every other query. statuses and phase are read ignoring case on the run, task, workflow, user, workspace and schedule lists.
  • A running task's log streams as it is written, through service-core and the Kubernetes dispatcher, instead of arriving whole when the task ends. Read before its container starts, it shows one waiting line and then the live log.
  • A catalogue task's Tekton YAML carries each param's default and description; a password default reads ***** and is kept when the YAML is applied back unchanged.
  • An upload- or download-artifact task is handed out to its dispatcher and completes with workspace quotas enforced, whatever the security setting. A task the dispatcher poll cannot prepare ends failed with reason DispatchPrepareFailed, and the rest of the poll is still handed out.
  • The tokens.<name> context parameters are gone. They held a token's stored hash, which cannot authenticate, under a name no reference could reach. A task that needs a credential takes it from a secret parameter.

Fixed

  • A declared or bootstrap token whose value a minted token already held made every request with it answer 500. The configured token now takes the value over and the minted one is deleted (logged at warn); two configured tokens sharing a value stop the start, and a value two stored tokens hold is refused (401), never a 500.
  • The webapp's sign-in answers 403 AUTH_USER_NOT_ADMITTED, not a 500, when Auto-Create Users is off and the person is not a user yet. The sign-in page now shows that refusal's own message ("Ask an admin to add you to a workspace") instead of blaming the identity provider.
  • PUT /api/v2/activate behind a proxy answers a wrong, missing or oversized activation code with 400 AUTH_ACTIVATION_CODE_INVALID and its message, as the OpenID Connect sign-in already did, instead of a bare 400. An error response whose status is not 401 no longer carries WWW-Authenticate.
  • A declared token's permissions are optional. With none, the token holds no grant and can call only routes that need no permission, which is the dispatcher protocol; any permission given is still validated. A dispatcher-only token no longer has to name a grant it never uses.
  • A change to the audit retention setting (audit retentionDays) through PATCH /api/v2/settings now reaches the audit expiry index on every instance within a minute (flow.audit.retention-interval-ms), instead of at the next restart. The 60-day floor is unchanged. The run (workflowrun retention.days) and task log retention settings already took effect without a restart.
  • A task's live log answers with a line saying what happened, instead of a bare 500, when the dispatcher holding the task cannot be reached (and no stored log counts) or its stream breaks part way.
  • service-core and service-dispatcher no longer crash at start with SIGILL on some machines. The JVM cached call adapters in the image's AOT cache, and an adapter recorded on the build machine could fail to link on another CPU; both images now record and start with adapter caching off. Class loading and linking stay cached.
  • Loki live task logs (dispatcher.logging.type=loki) select the labels the dispatcher sets on the task's pod, as a log shipper's labelmap names them (boomerang_io_taskrun_ref, boomerang_io_workflow_ref), and the task container of either executor (container=~"task|step-task"). The query asked for v3's bmrg_* labels and Tekton's container only, so it matched no v5 task. The log shipper must map pod labels and the container name under those names.
  • Slack's own install link (GET /api/v2/integration/slack/install) answers 409 INTEGRATION_NOT_CONFIGURED while the Slack app's client ID or secret is unset, instead of 404 INTEGRATION_INVALID_REF.
  • The API contract (contracts/api-v2.yaml) declares authentication: a bearer token or x-access-token on every route, none on the GitHub callback and Slack routes, and the access_token query parameter beside them on /webhook, /event and /callback. It no longer declares the System tag twice, reuses an operation summary, omits a path parameter or lists Spring's HttpHeaders as a request header.
  • Save refuses a node that asks for retries (retry.max above 0) of a type the engine never requeues, with WORKFLOW_INVALID_TASK_RETRY (1223) naming the task. Such a retry, on a runworkflow or sleep node for example, was stored and never applied; a run-workflow task's retries belong on the tasks of the workflow it runs. The editor already offered retry only on the requeueable types; a definition saved through the API with such a retry must drop it.
  • A for-each item takes its node's retry, with its own budget: a failed item is requeued with the node's backoff while its siblings' budgets are untouched, and the dispatcher's order states the budget, so an item's first attempt is no longer reported as its last. Items used to carry no retry and ended on their first failure.
  • Scheduled runs are admitted under workspace quota enforcement. A schedule names its workspace by id, and the quota lookups matched names only, so every fire failed before the run was created; scheduled runs also fell back to the platform's run-duration and workspace-size limits instead of the workspace's own. Quotas now find the workspace by id or name.
  • Concurrent first runs of a template in a workspace make one link, not one each: the children of a for-each runworkflow task all run as the same link. A linksTemplate relationship edge claims the workspace's link to the template. Workspaces that already hold duplicate links keep them; reads take the first, and each delete by name removes one.
  • A releaselock task frees the key an acquirelock in the same run took. It used to match only the acquiring task itself, so it succeeded without releasing anything, and the key stayed held until the run ended or the lease expired. A lock another run holds is still never touched.
  • The Acquire Lock task states its timeout in milliseconds, the unit the engine has always read it in, and defaults it to 300000 (5 minutes): revision 1 said seconds and defaulted to 300, a 300 ms lease. Change unit _0040__SeedAcquireLockMillisecondsRevision adds revision 2 to an install holding the task; a workflow pinned to revision 1, or one that stored the old default, keeps its value until it is edited.
  • A for-each item's params read $(params.item) and $(params.index) again: the item and index the engine sets at fan-out are context params, so the rule that a task param never reads the task's own params (2026-10-29) no longer fails every item with ParamUnresolved.
  • The Send Email with SendGrid Template task's revision 3 has no placeholder default for its API key, and takes the template ID as plain text; revision 2 defaulted both to a placeholder and masked the template ID. Change unit _0033__SeedSendGridTemplateRevision adds it to existing installs; a workflow moves when it is upgraded in the editor.
  • Disconnecting GitHub on the Integrations page disconnects it. POST /api/v2/integration/github/unlink takes the integration's record id or the installation id, refuses a caller who cannot reach the workspace (integration write permission in any workspace could unlink another workspace's GitHub by its installation id), answers 404 for an unknown installation, and is audited.
  • The GitHub integration dialog reads the installation and its repositories through the external HTTP client, so the proxy, trust and timeout settings apply, and it lists every repository the installation can reach.
  • An approval or manual task that a run cancel or a task timeout completed while its action was being created stays completed; it used to flip back to waiting with its action left open for nobody to answer.
  • A workflow token's principal is the workflow id everywhere: the editor issued it under the workflow name and the delete revoked by name, so deleting a workflow revoked the tokens of every workflow with that name in every workspace and left its own scheduler token in place.
  • Slack reaches its routes on a secured install. POST /api/v2/integration/slack/{commands,interactivity,events} and GET /api/v2/integration/slack/auth no longer require a Flow credential; each checks the request itself (the Slack signature, or the OAuth code exchange). GET /api/v2/integration/slack/install still requires sign-in, and the unused /api/v2/extensions/slack/install permit entry is gone.