@@ -470,6 +470,45 @@ func (c *SlackField) UnmarshalYAML(unmarshal func(any) error) error {
470470 return nil
471471}
472472
473+ // SlackMessageStrategy controls how subsequent notifications for the same
474+ // alert group are delivered to Slack.
475+ type SlackMessageStrategy string
476+
477+ const (
478+ // SlackMessageStrategyNew sends each notification as a separate message (default).
479+ SlackMessageStrategyNew SlackMessageStrategy = "new"
480+ // SlackMessageStrategyUpdate updates the original message in-place.
481+ SlackMessageStrategyUpdate SlackMessageStrategy = "update"
482+ // SlackMessageStrategyThread sends subsequent notifications as threaded replies.
483+ SlackMessageStrategyThread SlackMessageStrategy = "thread"
484+ )
485+
486+ // SlackThreadedOptions configures thread-specific behavior when message_strategy is "thread".
487+ type SlackThreadedOptions struct {
488+ // ResolveEmoji is the emoji name (without colons) to react with on the
489+ // original thread message when all alerts in the group are resolved.
490+ // Requires the bot token to have reactions:write scope.
491+ ResolveEmoji string `yaml:"resolve_emoji,omitempty" json:"resolve_emoji,omitempty"`
492+
493+ // UseSummaryHeader controls whether the thread parent is a lightweight
494+ // auto-updated summary (true, default) or the first actual alert message
495+ // (false). When true, all alert content is posted as replies and the parent
496+ // is continuously updated with the transition title and color.
497+ UseSummaryHeader * bool `yaml:"use_summary_header,omitempty" json:"use_summary_header,omitempty"`
498+
499+ // SummaryHeader holds options for summary-header mode only (see UseSummaryHeader).
500+ SummaryHeader * SlackThreadSummaryHeaderOptions `yaml:"summary_header,omitempty" json:"summary_header,omitempty"`
501+ }
502+
503+ // SlackThreadSummaryHeaderOptions configures fields that only apply when
504+ // message_strategy is "thread" and use_summary_header is true (the lightweight
505+ // parent summary mode).
506+ type SlackThreadSummaryHeaderOptions struct {
507+ // ResolveColor overrides the parent summary attachment color when the alert
508+ // group resolves. Supports Go templates.
509+ ResolveColor string `yaml:"resolve_color,omitempty" json:"resolve_color,omitempty"`
510+ }
511+
473512// SlackConfig configures notifications via Slack.
474513type SlackConfig struct {
475514 amcommoncfg.NotifierConfig `yaml:",inline" json:",inline"`
@@ -505,10 +544,19 @@ type SlackConfig struct {
505544 MrkdwnIn []string `yaml:"mrkdwn_in,omitempty" json:"mrkdwn_in,omitempty"`
506545 Actions []* SlackAction `yaml:"actions,omitempty" json:"actions,omitempty"`
507546
547+ // MessageStrategy controls how subsequent notifications for the same alert
548+ // group are delivered: "new" (default), "update" (edit in-place), or "thread"
549+ // (threaded replies). "update" and "thread" require a bot token.
550+ MessageStrategy SlackMessageStrategy `yaml:"message_strategy,omitempty" json:"message_strategy,omitempty"`
551+
508552 // UpdateMessage enables updating existing Slack messages instead of creating new ones.
509- // Requires bot token with chat:write scope. Webhook URLs do not support updates.
553+ // Deprecated: use message_strategy: update instead. If true, message_strategy must
554+ // be unset or "update"; when message_strategy is unset it is treated as "update".
555+ UpdateMessage bool `yaml:"update_message,omitempty" json:"update_message,omitempty"`
510556
511- UpdateMessage bool `yaml:"update_message" json:"update_message,omitempty"`
557+ // ThreadedOptions configures thread-specific behavior.
558+ // Only valid when message_strategy is "thread".
559+ ThreadedOptions * SlackThreadedOptions `yaml:"threaded_options,omitempty" json:"threaded_options,omitempty"`
512560 // Timeout is the maximum time allowed to invoke the slack. Setting this to 0
513561 // does not impose a timeout.
514562 Timeout time.Duration `yaml:"timeout" json:"timeout"`
@@ -532,13 +580,77 @@ func (c *SlackConfig) UnmarshalYAML(unmarshal func(any) error) error {
532580 return errors .New ("at most one of api_url/api_url_file & app_token/app_token_file must be configured" )
533581 }
534582
535- if c .UpdateMessage && c .APIURL .String () != "https://slack.com/api/chat.postMessage" {
536- return errors .New ("update_message can only be used with bot tokens. api_url must be set to https://slack.com/api/chat.postMessage" )
583+ // Deprecated: remove this block when update_message is deleted.
584+ if c .UpdateMessage {
585+ if c .MessageStrategy != "" && c .MessageStrategy != SlackMessageStrategyUpdate {
586+ return fmt .Errorf ("update_message: true is incompatible with message_strategy %q; omit message_strategy or set message_strategy: \" update\" " , c .MessageStrategy )
587+ }
588+ if c .MessageStrategy == "" {
589+ c .MessageStrategy = SlackMessageStrategyUpdate
590+ }
591+ }
592+ if c .MessageStrategy == "" {
593+ c .MessageStrategy = SlackMessageStrategyNew
594+ }
595+
596+ if c .MessageStrategy != SlackMessageStrategyNew &&
597+ c .MessageStrategy != SlackMessageStrategyUpdate &&
598+ c .MessageStrategy != SlackMessageStrategyThread {
599+ return fmt .Errorf ("unknown message_strategy %q; must be \" new\" , \" update\" , or \" thread\" " , c .MessageStrategy )
600+ }
601+
602+ if c .ThreadedOptions != nil {
603+ if c .MessageStrategy != SlackMessageStrategyThread {
604+ return errors .New ("threaded_options requires message_strategy to be \" thread\" " )
605+ }
606+ if c .ThreadedOptions .UseSummaryHeader != nil && ! * c .ThreadedOptions .UseSummaryHeader && c .ThreadedOptions .SummaryHeader != nil {
607+ return errors .New ("threaded_options.summary_header requires use_summary_header to be enabled" )
608+ }
537609 }
538610
539611 return nil
540612}
541613
614+ // ValidateMessageStrategy checks that the resolved api_url (after global defaults
615+ // have been applied) satisfies the requirements for update/thread strategies.
616+ func (c * SlackConfig ) ValidateMessageStrategy () error {
617+ switch c .MessageStrategy {
618+ case SlackMessageStrategyUpdate , SlackMessageStrategyThread :
619+ if c .APIURL == nil && c .APIURLFile == "" {
620+ return fmt .Errorf ("message_strategy %q requires api_url or api_url_file" , c .MessageStrategy )
621+ }
622+ if c .APIURL != nil && c .APIURL .String () != "https://slack.com/api/chat.postMessage" {
623+ return fmt .Errorf ("message_strategy %q requires a bot token; api_url must be https://slack.com/api/chat.postMessage" , c .MessageStrategy )
624+ }
625+ }
626+ return nil
627+ }
628+
629+ // UseSummaryHeaderInThread returns true when the thread parent should be a lightweight
630+ // auto-updated summary. Returns true by default (nil or explicit true).
631+ func (c * SlackConfig ) UseSummaryHeaderInThread () bool {
632+ if c .ThreadedOptions == nil || c .ThreadedOptions .UseSummaryHeader == nil {
633+ return true
634+ }
635+ return * c .ThreadedOptions .UseSummaryHeader
636+ }
637+
638+ // HasStrategyThatUpdatesParent reports whether message_strategy uses nflog to tie
639+ // multiple notifications to one Slack message or thread.
640+ func (c * SlackConfig ) HasStrategyThatUpdatesParent () bool {
641+ return c .HasUpdateStrategy () || c .HasThreadStrategy ()
642+ }
643+
644+ // HasUpdateStrategy is true when message_strategy is "update" (chat.update in place).
645+ func (c * SlackConfig ) HasUpdateStrategy () bool {
646+ return c .MessageStrategy == SlackMessageStrategyUpdate
647+ }
648+
649+ // HasThreadStrategy is true when message_strategy is "thread" (threaded replies).
650+ func (c * SlackConfig ) HasThreadStrategy () bool {
651+ return c .MessageStrategy == SlackMessageStrategyThread
652+ }
653+
542654// IncidentioConfig configures notifications via incident.io.
543655type IncidentioConfig struct {
544656 amcommoncfg.NotifierConfig `yaml:",inline" json:",inline"`
0 commit comments