feat: question update --db; fix Slack alerts/notifications for v0.59+#21
Merged
rohanraarora merged 2 commits intoJun 2, 2026
Conversation
Two related gaps in the CLI that make it impossible to do an in-place
database swap on a saved card, or to set up a Slack alert that the
Metabase v0.59+ notification API actually accepts.
## question update --db <id>
The CLI exposed --name, --sql, --display, etc. on `question update`, but
not a way to change the underlying database. Setting only the top-level
database_id via PUT /api/card/{id} leaves dataset_query.database
pointing at the original DB, so the card keeps running against the old
source even after the update succeeds. New flag updates both fields so
they stay consistent. Factored out applyDatabaseToDatasetQuery as a
pure helper with regression tests covering v0.59+ stages and legacy
native shapes.
## Slack alerts / notifications
Three coupled bugs that together made `--channel-type slack`
unsupported on Metabase v0.59+:
1. channel_type was sent as the bare "slack"/"email" string. v0.59+
requires the "channel/" prefix; the bare form fails with
`{"specific-errors":{"handlers":[{"channel_type":
["unknown error, received: :slack"]}]}}`. The new
canonicalizeChannelType helper accepts both forms and emits the
prefixed one.
2. recipients only supported user-id targets (always emitting
`notification-recipient/user`). Slack channel-name targets need a
`notification-recipient/raw-value` recipient with
`details.value: "#channel"`. Added --slack-channel <name> on
`alert create`, `alert update`, and `notification create`, and
surfaced helpers (slackChannelRecipient, userRecipient,
cronSubscription) on the API module so library users can compose
handlers correctly too.
3. Schedule was attached to handlers[].schedule. v0.59+ moved
scheduling to top-level subscriptions[] as
`notification-subscription/cron` with a cron_schedule string.
translateChannelsToSubscriptions converts both the legacy
schedule_type/schedule_hour cadence and a new --schedule <cron>
flag to the new shape; the legacy form is translated to Quartz
cron internally for back-compat.
Also renamed the payload fields the API now expects:
alert_condition + alert_above_goal -> send_condition (has_result /
goal_above / goal_below), alert_first_only -> send_once. Both legacy
field names still work as CLI input; the translation happens inside
AlertApi.
## Verified end-to-end
- `metabase-cli question update 24355 --db 35 --unsafe` now updates
database_id AND dataset_query.database in one PUT; the card is
immediately runnable against the new DB.
- `metabase-cli alert create --card 24355 --condition rows
--channel-type slack --slack-channel "#nsdc-assessment-alert"
--schedule "0 0 * * * ?"` creates alert #58 against
v0.59.4 (d4fb593), with the v0.59+ canonical body:
handlers: [{
channel_type: "channel/slack",
recipients: [{
type: "notification-recipient/raw-value",
details: { value: "#nsdc-assessment-alert" }
}]
}],
subscriptions: [{
type: "notification-subscription/cron",
cron_schedule: "0 0 * * * ?"
}],
payload: { send_condition: "has_result", send_once: false }
## Tests
Full suite stays green: 142 passing (138 baseline + 4 new). New
coverage: applyDatabaseToDatasetQuery on v0.59+ stages and legacy
native shapes, and an AlertApi.create regression that asserts
channel/slack + raw-value recipient + top-level cron subscription on
the outgoing body.
Two review follow-ups on the v0.59+ alert work: - alert create: canonicalize --channel-type before the Slack-recipient guard so the prefixed form (channel/slack) is validated the same as the bare form (slack). Previously the guard was skipped for the prefixed form and an empty-recipient Slack handler was sent. - AlertApi.update: when only --above-goal is supplied (no --condition), imply a goal condition instead of falling back to rows/has_result, which silently downgraded a goal alert. Adds regression tests for both, plus canonicalizeChannelType idempotency.
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two related gaps that together make it impossible to use the CLI for two pretty common Metabase v0.59+ workflows:
question updatelets you change name, SQL, display, viz, template tags… but not the underlying database. Settingdatabase_idalone onPUT /api/card/{id}silently leavesdataset_query.databasepointing at the old DB and the card keeps running against the original source.alert create --channel-type slackis rejected by v0.59+ Metabase with400 Bad Request: {"specific-errors":{"handlers":[{"channel_type":["unknown error, received: :slack"]}]}}. Three coupled issues — wrongchannel_typeliteral, no way to specify a Slack channel name, schedule attached to the wrong field — make Slack alerts unreachable from the CLI.What changes
question update --db <id>New flag on
question update. Updates both the top-leveldatabase_idanddataset_query.databasein onePUT, so the two never drift. Pure helperapplyDatabaseToDatasetQuerycovers v0.59+ stages and legacy native shapes; new tests assert both.Slack alerts / notifications
Three coupled fixes against
/api/notification:channel_typesent as bare"slack"/"email"(rejected by v0.59+)canonicalizeChannelType()accepts bare or prefixed input and emitschannel/slack/channel/emailrecipientshardcoded tonotification-recipient/user(can't address a Slack channel by name)--slack-channel <name>flag emits anotification-recipient/raw-valuewithdetails.value: "#channel"--schedule <cron>was attached tohandlers[].schedule(silently ignored)subscriptions[]asnotification-subscription/cron; new--schedule-type/--schedule-hourfall through to cron for legacy callersAlso wired through the renamed payload fields:
alert_condition+alert_above_goal→send_condition(has_result/goal_above/goal_below),alert_first_only→send_once. Legacy field names are still accepted as CLI input.Verified end-to-end against v0.59.4 (d4fb593)
Reproducing the original
unknown error, received: :slack400 onmain, then running the new code against the same instance:And the database swap:
Tests
Full suite stays green: 142 passing (138 baseline + 4 new).
applyDatabaseToDatasetQueryon v0.59+ stages and legacy native shapes; refuses to mutate input.AlertApi.createregression that assertschannel/slack+ raw-value Slack recipient + top-levelnotification-subscription/cronon the outgoing body.AlertApi.create/AlertApi.updatetests reflect the renamedsend_condition/send_oncefields.Lint passes; build succeeds.
Back-compat
--channel-type slack(bare) still works — internally canonicalized tochannel/slack.schedule_type/schedule_hourstill work — translated to Quartz cron for the newsubscriptions[]shape.alert_condition/alert_first_onlystill accepted as CLI input — mapped tosend_condition/send_onceinsideAlertApi.--dbflag are unchanged.🤖 Generated with Claude Code