@@ -520,6 +520,45 @@ func (c *SlackField) UnmarshalYAML(unmarshal func(any) error) error {
520520 return nil
521521}
522522
523+ // SlackMessageStrategy controls how subsequent notifications for the same
524+ // alert group are delivered to Slack.
525+ type SlackMessageStrategy string
526+
527+ const (
528+ // SlackMessageStrategyNew sends each notification as a separate message (default).
529+ SlackMessageStrategyNew SlackMessageStrategy = "new"
530+ // SlackMessageStrategyUpdate updates the original message in-place.
531+ SlackMessageStrategyUpdate SlackMessageStrategy = "update"
532+ // SlackMessageStrategyThread sends subsequent notifications as threaded replies.
533+ SlackMessageStrategyThread SlackMessageStrategy = "thread"
534+ )
535+
536+ // SlackThreadedOptions configures thread-specific behavior when message_strategy is "thread".
537+ type SlackThreadedOptions struct {
538+ // ResolveEmoji is the emoji name (without colons) to react with on the
539+ // original thread message when all alerts in the group are resolved.
540+ // Requires the bot token to have reactions:write scope.
541+ ResolveEmoji string `yaml:"resolve_emoji,omitempty" json:"resolve_emoji,omitempty"`
542+
543+ // UseSummaryHeader controls whether the thread parent is a lightweight
544+ // auto-updated summary (true, default) or the first actual alert message
545+ // (false). When true, all alert content is posted as replies and the parent
546+ // is continuously updated with the transition title and color.
547+ UseSummaryHeader * bool `yaml:"use_summary_header,omitempty" json:"use_summary_header,omitempty"`
548+
549+ // SummaryHeader holds options for summary-header mode only (see UseSummaryHeader).
550+ SummaryHeader * SlackThreadSummaryHeaderOptions `yaml:"summary_header,omitempty" json:"summary_header,omitempty"`
551+ }
552+
553+ // SlackThreadSummaryHeaderOptions configures fields that only apply when
554+ // message_strategy is "thread" and use_summary_header is true (the lightweight
555+ // parent summary mode).
556+ type SlackThreadSummaryHeaderOptions struct {
557+ // ResolveColor overrides the parent summary attachment color when the alert
558+ // group resolves. Supports Go templates.
559+ ResolveColor string `yaml:"resolve_color,omitempty" json:"resolve_color,omitempty"`
560+ }
561+
523562// SlackConfig configures notifications via Slack.
524563type SlackConfig struct {
525564 amcommoncfg.NotifierConfig `yaml:",inline" json:",inline"`
@@ -555,10 +594,19 @@ type SlackConfig struct {
555594 MrkdwnIn []string `yaml:"mrkdwn_in,omitempty" json:"mrkdwn_in,omitempty"`
556595 Actions []* SlackAction `yaml:"actions,omitempty" json:"actions,omitempty"`
557596
597+ // MessageStrategy controls how subsequent notifications for the same alert
598+ // group are delivered: "new" (default), "update" (edit in-place), or "thread"
599+ // (threaded replies). "update" and "thread" require a bot token.
600+ MessageStrategy SlackMessageStrategy `yaml:"message_strategy,omitempty" json:"message_strategy,omitempty"`
601+
558602 // UpdateMessage enables updating existing Slack messages instead of creating new ones.
559- // Requires bot token with chat:write scope. Webhook URLs do not support updates.
603+ // Deprecated: use message_strategy: update instead. If true, message_strategy must
604+ // be unset or "update"; when message_strategy is unset it is treated as "update".
605+ UpdateMessage bool `yaml:"update_message,omitempty" json:"update_message,omitempty"`
560606
561- UpdateMessage bool `yaml:"update_message" json:"update_message,omitempty"`
607+ // ThreadedOptions configures thread-specific behavior.
608+ // Only valid when message_strategy is "thread".
609+ ThreadedOptions * SlackThreadedOptions `yaml:"threaded_options,omitempty" json:"threaded_options,omitempty"`
562610 // Timeout is the maximum time allowed to invoke the slack. Setting this to 0
563611 // does not impose a timeout.
564612 Timeout time.Duration `yaml:"timeout" json:"timeout"`
@@ -582,13 +630,77 @@ func (c *SlackConfig) UnmarshalYAML(unmarshal func(any) error) error {
582630 return errors .New ("at most one of api_url/api_url_file & app_token/app_token_file must be configured" )
583631 }
584632
585- if c .UpdateMessage && c .APIURL .String () != "https://slack.com/api/chat.postMessage" {
586- return errors .New ("update_message can only be used with bot tokens. api_url must be set to https://slack.com/api/chat.postMessage" )
633+ // Deprecated: remove this block when update_message is deleted.
634+ if c .UpdateMessage {
635+ if c .MessageStrategy != "" && c .MessageStrategy != SlackMessageStrategyUpdate {
636+ return fmt .Errorf ("update_message: true is incompatible with message_strategy %q; omit message_strategy or set message_strategy: \" update\" " , c .MessageStrategy )
637+ }
638+ if c .MessageStrategy == "" {
639+ c .MessageStrategy = SlackMessageStrategyUpdate
640+ }
641+ }
642+ if c .MessageStrategy == "" {
643+ c .MessageStrategy = SlackMessageStrategyNew
644+ }
645+
646+ if c .MessageStrategy != SlackMessageStrategyNew &&
647+ c .MessageStrategy != SlackMessageStrategyUpdate &&
648+ c .MessageStrategy != SlackMessageStrategyThread {
649+ return fmt .Errorf ("unknown message_strategy %q; must be \" new\" , \" update\" , or \" thread\" " , c .MessageStrategy )
650+ }
651+
652+ if c .ThreadedOptions != nil {
653+ if c .MessageStrategy != SlackMessageStrategyThread {
654+ return errors .New ("threaded_options requires message_strategy to be \" thread\" " )
655+ }
656+ if c .ThreadedOptions .UseSummaryHeader != nil && ! * c .ThreadedOptions .UseSummaryHeader && c .ThreadedOptions .SummaryHeader != nil {
657+ return errors .New ("threaded_options.summary_header requires use_summary_header to be enabled" )
658+ }
587659 }
588660
589661 return nil
590662}
591663
664+ // ValidateMessageStrategy checks that the resolved api_url (after global defaults
665+ // have been applied) satisfies the requirements for update/thread strategies.
666+ func (c * SlackConfig ) ValidateMessageStrategy () error {
667+ switch c .MessageStrategy {
668+ case SlackMessageStrategyUpdate , SlackMessageStrategyThread :
669+ if c .APIURL == nil && c .APIURLFile == "" {
670+ return fmt .Errorf ("message_strategy %q requires api_url or api_url_file" , c .MessageStrategy )
671+ }
672+ if c .APIURL != nil && c .APIURL .String () != "https://slack.com/api/chat.postMessage" {
673+ return fmt .Errorf ("message_strategy %q requires a bot token; api_url must be https://slack.com/api/chat.postMessage" , c .MessageStrategy )
674+ }
675+ }
676+ return nil
677+ }
678+
679+ // UseSummaryHeaderInThread returns true when the thread parent should be a lightweight
680+ // auto-updated summary. Returns true by default (nil or explicit true).
681+ func (c * SlackConfig ) UseSummaryHeaderInThread () bool {
682+ if c .ThreadedOptions == nil || c .ThreadedOptions .UseSummaryHeader == nil {
683+ return true
684+ }
685+ return * c .ThreadedOptions .UseSummaryHeader
686+ }
687+
688+ // HasStrategyThatUpdatesParent reports whether message_strategy uses nflog to tie
689+ // multiple notifications to one Slack message or thread.
690+ func (c * SlackConfig ) HasStrategyThatUpdatesParent () bool {
691+ return c .HasUpdateStrategy () || c .HasThreadStrategy ()
692+ }
693+
694+ // HasUpdateStrategy is true when message_strategy is "update" (chat.update in place).
695+ func (c * SlackConfig ) HasUpdateStrategy () bool {
696+ return c .MessageStrategy == SlackMessageStrategyUpdate
697+ }
698+
699+ // HasThreadStrategy is true when message_strategy is "thread" (threaded replies).
700+ func (c * SlackConfig ) HasThreadStrategy () bool {
701+ return c .MessageStrategy == SlackMessageStrategyThread
702+ }
703+
592704// IncidentioConfig configures notifications via incident.io.
593705type IncidentioConfig struct {
594706 amcommoncfg.NotifierConfig `yaml:",inline" json:",inline"`
0 commit comments