<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Rost</title>
    <description>The latest articles on DEV Community by Rost (@rosgluk).</description>
    <link>https://dev.to/rosgluk</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3544400%2F04dd81bf-749e-4055-971f-316c0134e76c.jpg</url>
      <title>DEV Community: Rost</title>
      <link>https://dev.to/rosgluk</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rosgluk"/>
    <language>en</language>
    <item>
      <title>OpenClaw Plugins — Ecosystem Guide and Practical Picks</title>
      <dc:creator>Rost</dc:creator>
      <pubDate>Mon, 20 Apr 2026 09:51:41 +0000</pubDate>
      <link>https://dev.to/rosgluk/openclaw-plugins-ecosystem-guide-and-practical-picks-4an1</link>
      <guid>https://dev.to/rosgluk/openclaw-plugins-ecosystem-guide-and-practical-picks-4an1</guid>
      <description>&lt;p&gt;This article is about &lt;strong&gt;OpenClaw plugins&lt;/strong&gt; — native gateway packages that add channels, model providers, tools, speech, memory, media, web search, and other runtime surfaces.&lt;/p&gt;

&lt;p&gt;The rest of the piece covers discovery, packaging, CLI lifecycle, maturity, security, and concrete plugin picks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OpenClaw skills&lt;/strong&gt; matter for navigation and safety because ClawHub and announcement text often say "skills" when they mean installable agent packs and workflows. Those are related to the same registries you use for plugins, but they are not the same mechanism as a validated &lt;code&gt;openclaw.plugin.json&lt;/code&gt; package. The glossary below keeps the vocabulary straight; the &lt;a href="https://www.glukhov.org/ai-systems/openclaw/skills/" rel="noopener noreferrer"&gt;OpenClaw skills guide&lt;/a&gt; goes deeper on authoring, moderation, usage patterns, and per-role stacks.&lt;/p&gt;

&lt;p&gt;At the same time, the public plugin ecosystem is uneven.&lt;br&gt;
The strongest parts are still bundled first-party surfaces and a small set of community plugins with visible maintenance and usage. The weaker parts are the business-automation edge cases that look impressive in demos but still have thin public adoption signals, including skill-oriented repos that are not yet mature native plugins.&lt;/p&gt;

&lt;p&gt;If you want the short version up front, this is it.&lt;br&gt;
In OpenClaw today, the "actually useful" plugin layer is mostly about boring wins: browser access, web extraction, memory, provider routing, voice, channels, observability, and workflow triggers. The categories that sound most enterprise-friendly — CRM, lead generation, inbox automation, calendar orchestration — do exist publicly, but the verified native-plugin surface is still much thinner and less battle-tested than the rest of the stack. That is not a criticism so much as a maturity signal.&lt;/p&gt;
&lt;h3&gt;
  
  
  Glossary (plugins, extensions, skills)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;OpenClaw plugins&lt;/strong&gt; — Native gateway packages installed with &lt;code&gt;openclaw plugins …&lt;/code&gt;, validated through &lt;code&gt;openclaw.plugin.json&lt;/code&gt;, and able to register channels, providers, tools, memory backends, and other hooks inside the gateway process.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OpenClaw extensions&lt;/strong&gt; — Workspace and global directories OpenClaw scans as plugin roots before bundled defaults (extension paths under the workspace, then &lt;code&gt;~/.openclaw&lt;/code&gt;). This is a layout and discovery idea. It is not a different kind of artifact from plugins; it is where plugin packages are loaded from.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OpenClaw skills&lt;/strong&gt; — Agent-facing packs and workflows often published for OpenClaw-style agents and listed on ClawHub alongside packages. Security and moderation messaging frequently refers to "skills" because that layer has its own adoption and abuse history. Treat skills as a related install surface, not as a synonym for "native plugin" unless the listing is actually a plugin package with a manifest.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When OpenClaw imports content from Codex, Claude, or Cursor ecosystems, upstream docs often call those &lt;strong&gt;bundles&lt;/strong&gt;, not native plugins. Bundles map to selective features and a narrower trust boundary than full plugins. If you mix bundles, skills marketing, and native OpenClaw plugins without that distinction, the ecosystem looks broader than it actually is.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why this ecosystem matters
&lt;/h2&gt;

&lt;p&gt;Inside the codebase and CLI, the extension story is still expressed as plugins. Discovery walks explicit config paths, then extension directories, then bundled plugins — same capability type, different roots. Skills enter the picture when you browse ClawHub or read incident writeups, not when you reason about slot selection for &lt;code&gt;memory&lt;/code&gt; or &lt;code&gt;contextEngine&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The plugin system is also opinionated in a useful way. OpenClaw does not treat plugins as a cosmetic add-on layer. It uses them for concrete runtime ownership: channels, model providers, tools, memory backends, context engines, speech, realtime voice, media understanding, image generation, video generation, web fetch, and web search. Some of those ship bundled inside OpenClaw, while others are external packages published by the community on npm or ClawHub.&lt;/p&gt;

&lt;p&gt;That is why the plugin ecosystem matters more than it first appears. In practice, plugin choice determines not just integrations, but also how the assistant searches, remembers, calls, routes, fetches, traces, and survives long-running sessions. For a technical blog, that is the important frame. Not "which package looks cool", but "which package owns a meaningful runtime surface".&lt;/p&gt;
&lt;h2&gt;
  
  
  How the plugin system actually works
&lt;/h2&gt;

&lt;p&gt;Under the hood, OpenClaw discovers plugins in a fixed order, and first match wins. It looks at explicit config paths first, then workspace extension directories, then global extensions under &lt;code&gt;~/.openclaw&lt;/code&gt;, and finally bundled plugins that ship with OpenClaw. Workspace-origin plugins are disabled by default, restrictive allowlists can block even bundled plugins, and some capability classes are exclusive slots, notably &lt;code&gt;memory&lt;/code&gt; and &lt;code&gt;contextEngine&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That slot model is one of the least flashy but most important parts of the system. It means plugins are not only additive. In some categories they are selectors. &lt;code&gt;memory-core&lt;/code&gt; can be the active memory plugin, &lt;code&gt;memory-lancedb&lt;/code&gt; can replace it, and a context engine such as &lt;code&gt;lossless-claw&lt;/code&gt; can replace the default legacy context engine. This is why memory plugins tend to matter operationally more than UI-facing plugins. They change how the assistant thinks across time, not just where it sends messages.&lt;/p&gt;

&lt;p&gt;Native plugins also have a fairly strict packaging model. A package advertises its plugin entrypoints and setup metadata through &lt;code&gt;package.json&lt;/code&gt;, while &lt;code&gt;openclaw.plugin.json&lt;/code&gt; is the manifest OpenClaw uses to validate plugin identity and config before executing plugin code. That manifest is not decorative. Missing or invalid manifests are treated as plugin errors and block config validation. The platform is clearly trying to fail early rather than load first and hope later.&lt;/p&gt;

&lt;p&gt;The SDK surface is broader than many blog posts imply. Plugin hooks can intercept model resolution, agent lifecycle, message flow, tool execution, sub-agent coordination, and gateway lifecycle, and the docs state that the SDK exposes 28 hooks. That is enough power to build real runtime products, but it is also enough power to create runtime surprises if the plugin is immature.&lt;/p&gt;
&lt;h2&gt;
  
  
  Where to get plugins and how the lifecycle works
&lt;/h2&gt;

&lt;p&gt;Plugin installs always go through the &lt;code&gt;openclaw plugins&lt;/code&gt; commands below. ClawHub lists both native plugin packages and OpenClaw skills-style entries, so read each listing for manifests and supported install paths — this section is only about the plugin path.&lt;/p&gt;

&lt;p&gt;The public repository layer is straightforward. ClawHub is the canonical discovery surface for community plugins and many skills listings, and OpenClaw can install plugins from ClawHub, npm, local paths, local archives, and supported marketplaces. For bare package names, OpenClaw checks ClawHub first and falls back to npm automatically. That alone answers one of the common ecosystem questions: yes, there is a public repository story, but it is split between the official registry layer and npm.&lt;/p&gt;

&lt;p&gt;The install and removal lifecycle is also clearer than the ecosystem chatter makes it sound. The CLI supports listing, inspecting, enabling, disabling, uninstalling, doctoring, and updating plugins. Config changes require a gateway restart, although the default &lt;code&gt;openclaw gateway&lt;/code&gt; path may auto-restart after a config write lands. In practice, temporary removal is &lt;code&gt;disable&lt;/code&gt;, hard removal is &lt;code&gt;uninstall&lt;/code&gt;, and validation failures are designed to fail closed instead of leaving half-installed state behind.&lt;/p&gt;

&lt;p&gt;The commands you actually need are simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openclaw plugins list
openclaw plugins inspect &amp;lt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
openclaw plugins &lt;span class="nb"&gt;install&lt;/span&gt; &amp;lt;package&amp;gt;
openclaw plugins &lt;span class="nb"&gt;enable&lt;/span&gt; &amp;lt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
openclaw plugins disable &amp;lt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
openclaw plugins uninstall &amp;lt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
openclaw gateway restart
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those commands are the stable part. The interesting parts are the safety rails around them. OpenClaw recommends pinned versions for plugin installs, uses &lt;code&gt;--ignore-scripts&lt;/code&gt; for npm dependency installs, validates compatibility metadata such as &lt;code&gt;pluginApi&lt;/code&gt; and &lt;code&gt;minGatewayVersion&lt;/code&gt; before archive installs, and ships a built-in dangerous-code scanner with a break-glass override named &lt;code&gt;--dangerously-force-unsafe-install&lt;/code&gt;. That is a more serious security posture than many agent ecosystems currently offer.&lt;/p&gt;

&lt;p&gt;One subtle detail is worth calling out. ClawHub install counts are useful, but they are not absolute ecosystem census numbers. The documentation says install counts are computed when logged-in users run &lt;code&gt;clawhub sync&lt;/code&gt;, and stale roots stop counting after 120 days. That makes ClawHub usage counters directionally useful, especially for ranking, but not a universal measure of actual adoption.&lt;/p&gt;

&lt;h2&gt;
  
  
  Maturity, support, and security reality
&lt;/h2&gt;

&lt;p&gt;The maturity story is split in two. First-party bundled plugins are the safest default because they live inside the main OpenClaw release train, share the same compatibility model, and benefit from a very large public core repository footprint. At crawl time, the main &lt;code&gt;openclaw/openclaw&lt;/code&gt; repository showed roughly 359k GitHub stars, which is the strongest public popularity signal anywhere in this ecosystem. Community plugins can absolutely be useful, but they are not all equal and they do not inherit that maturity automatically.&lt;/p&gt;

&lt;p&gt;OpenClaw's own community-plugin page is refreshingly blunt about the quality bar. The project asks for a public GitHub repository, working installation through &lt;code&gt;openclaw plugins install&lt;/code&gt;, setup and usage docs, and active maintenance. Low-effort wrappers, unclear ownership, or unmaintained packages may be declined. That tells you a lot about where the team has already seen ecosystem failure.&lt;/p&gt;

&lt;p&gt;Security is the part where opinion should replace hype. The docs themselves say to treat OpenClaw plugin installs like running code. ClawHub exposes moderation hooks, stars, comments, and usage signals, and the broader OpenClaw security response has moved toward stronger package scrutiny. The team announced VirusTotal scanning for all ClawHub skills, and independent security research documented malicious ClawHub campaigns and large-scale insecure credential handling in early 2026. Those incidents were centered on OpenClaw skills and skill-style listings, not every native plugin path, but they are still the correct backdrop for evaluating the whole installable ecosystem. The lesson is simple: the extension perimeter — config, OpenClaw extensions directories, and anything you install from a registry — is now part of the attack surface.&lt;/p&gt;

&lt;p&gt;A second, more nuanced security point is that safer ecosystems still produce false positives. OpenClaw's dangerous-code scanner is heuristic, and public plugin maintainers have already had to react to scanner warnings and installation friction. That is not a sign that the scanner is wrong to exist. It is a sign that "scanner clean" and "safe" are not identical concepts, and that human review still matters for nontrivial plugins.&lt;/p&gt;

&lt;h2&gt;
  
  
  Useful plugins worth tracking right now
&lt;/h2&gt;

&lt;p&gt;What follows is the pragmatic list, not the maximal list. For bundled first-party plugins that do not have standalone repos, the popularity metric below uses the OpenClaw core repo star count as the proxy. For community plugins, the popularity metric uses the canonical public GitHub repo star count visible at crawl time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tools and web access&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;browser&lt;/code&gt;&lt;br&gt;&lt;br&gt;
URL: &lt;code&gt;https://docs.openclaw.ai/tools/browser&lt;/code&gt;&lt;br&gt;&lt;br&gt;
This is the default serious-tool plugin because it gives the agent a managed isolated browser profile and an attach-to-user-browser mode when logged-in human sessions matter. That is more useful than another generic web search wrapper. Popularity: bundled first-party plugin, proxy metric 359k core repo stars.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;firecrawl&lt;/code&gt;&lt;br&gt;&lt;br&gt;
URL: &lt;code&gt;https://docs.openclaw.ai/tools/firecrawl&lt;/code&gt;&lt;br&gt;&lt;br&gt;
Firecrawl is useful because it can act as a &lt;code&gt;web_search&lt;/code&gt; provider, expose explicit &lt;code&gt;firecrawl_search&lt;/code&gt; and &lt;code&gt;firecrawl_scrape&lt;/code&gt; tools, and serve as a &lt;code&gt;web_fetch&lt;/code&gt; fallback for JS-heavy or anti-bot pages. Popularity: bundled first-party plugin, proxy metric 359k core repo stars.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;tavily&lt;/code&gt;&lt;br&gt;&lt;br&gt;
URL: &lt;code&gt;https://docs.openclaw.ai/tools/tavily&lt;/code&gt;&lt;br&gt;&lt;br&gt;
Tavily is still one of the cleaner structured-search options because it exposes both search and extraction and is explicitly optimized for LLM consumption. Popularity: bundled first-party plugin, proxy metric 359k core repo stars.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;exa&lt;/code&gt;&lt;br&gt;&lt;br&gt;
URL: &lt;code&gt;https://docs.openclaw.ai/tools/exa-search&lt;/code&gt;&lt;br&gt;&lt;br&gt;
Exa is the best fit when you want hybrid search modes plus extraction in one provider without immediately jumping to browser automation. Popularity: bundled first-party plugin, proxy metric 359k core repo stars.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Integrations and collaboration&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;matrix&lt;/code&gt;&lt;br&gt;&lt;br&gt;
URL: &lt;code&gt;https://docs.openclaw.ai/channels/matrix&lt;/code&gt;&lt;br&gt;&lt;br&gt;
Matrix is one of the more complete bundled collaboration plugins because it already supports DMs, rooms, threads, media, reactions, polls, location, and E2EE through &lt;code&gt;matrix-js-sdk&lt;/code&gt;. Popularity: bundled first-party plugin, proxy metric 359k core repo stars.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;msteams&lt;/code&gt;&lt;br&gt;&lt;br&gt;
URL: &lt;code&gt;https://docs.openclaw.ai/channels/msteams&lt;/code&gt;&lt;br&gt;&lt;br&gt;
Teams matters because it is one of the few enterprise channels with a real first-party path, including Azure Bot setup, tenant credentials, default webhook shape, and group-chat policy controls. Popularity: bundled first-party plugin, proxy metric 359k core repo stars.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;wecom&lt;/code&gt;&lt;br&gt;&lt;br&gt;
URL: &lt;code&gt;https://github.com/WecomTeam/wecom-openclaw-plugin&lt;/code&gt;&lt;br&gt;&lt;br&gt;
WeCom is one of the stronger community channel plugins because it is officially maintained by the Tencent WeCom team and supports direct messages, group chats, streaming replies, proactive messaging, and both Bot and Agent operating modes. Popularity: about 365 GitHub stars.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;openclaw-discourse&lt;/code&gt;&lt;br&gt;&lt;br&gt;
URL: &lt;code&gt;https://github.com/pranciskus/discourse-openclaw&lt;/code&gt;&lt;br&gt;&lt;br&gt;
Discourse is a good example of a plugin that is small but useful. It focuses on searching, reading, filtering, finding unanswered topics, and optionally writing back to the forum, which is exactly what support and community workflows need. Popularity: about 10 GitHub stars.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A side note here is that Slack is less interesting in a plugins article than many people expect, because Slack is already treated as a built-in channel surface in current OpenClaw documentation and marketing copy. Teams and WeCom are more revealing plugin picks because they show where external or bundled channel ownership still matters visibly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Memory and context&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;memory-lancedb&lt;/code&gt;&lt;br&gt;&lt;br&gt;
URL: &lt;code&gt;https://docs.openclaw.ai/tools/plugin&lt;/code&gt;&lt;br&gt;&lt;br&gt;
This is the practical long-session memory pick in the bundled set. OpenClaw describes it as an install-on-demand long-term memory plugin with auto-recall and capture, selected through &lt;code&gt;plugins.slots.memory&lt;/code&gt;. Popularity: bundled first-party plugin, proxy metric 359k core repo stars.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;memory-wiki&lt;/code&gt;&lt;br&gt;&lt;br&gt;
URL: &lt;code&gt;https://docs.openclaw.ai/plugins/memory-wiki&lt;/code&gt;&lt;br&gt;&lt;br&gt;
&lt;code&gt;memory-wiki&lt;/code&gt; is not a replacement memory backend. It is a companion plugin that compiles durable memory into a navigable wiki with provenance, contradictions, dashboards, and wiki-native search and apply tools. That makes it more useful for knowledge maintenance than raw recall alone. Popularity: bundled first-party plugin, proxy metric 359k core repo stars.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;lossless-claw&lt;/code&gt;&lt;br&gt;&lt;br&gt;
URL: &lt;code&gt;https://github.com/Martian-Engineering/lossless-claw&lt;/code&gt;&lt;br&gt;&lt;br&gt;
This is probably the most important community memory-context plugin right now. It replaces sliding-window compaction with DAG-based summarization that preserves full conversation history while keeping active context inside token limits. Popularity: about 4.3k GitHub stars.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;memos-cloud&lt;/code&gt;&lt;br&gt;&lt;br&gt;
URL: &lt;code&gt;https://github.com/MemTensor/MemOS-Cloud-OpenClaw-Plugin&lt;/code&gt;&lt;br&gt;&lt;br&gt;
MemOS Cloud is noteworthy because it treats memory as a lifecycle plugin, recalling context before execution and saving results after each run. That makes it closer to persistent memory infrastructure than a note store. Popularity: about 339 GitHub stars.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Model providers and harnesses&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;openai&lt;/code&gt;&lt;br&gt;&lt;br&gt;
URL: &lt;code&gt;https://docs.openclaw.ai/providers/openai&lt;/code&gt;&lt;br&gt;&lt;br&gt;
The OpenAI provider remains useful mostly because OpenClaw separates direct API access via &lt;code&gt;openai/*&lt;/code&gt; from ChatGPT or Codex OAuth via &lt;code&gt;openai-codex/*&lt;/code&gt;, which avoids a lot of confusion around billing and runtime path. Popularity: bundled first-party plugin, proxy metric 359k core repo stars.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;anthropic&lt;/code&gt;&lt;br&gt;&lt;br&gt;
URL: &lt;code&gt;https://docs.openclaw.ai/providers/anthropic&lt;/code&gt;&lt;br&gt;&lt;br&gt;
Anthropic is useful because OpenClaw supports both API keys and Claude CLI reuse, while still documenting API keys as the clearest long-lived gateway path. Popularity: bundled first-party plugin, proxy metric 359k core repo stars.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;openrouter&lt;/code&gt;&lt;br&gt;&lt;br&gt;
URL: &lt;code&gt;https://docs.openclaw.ai/providers/openrouter&lt;/code&gt;&lt;br&gt;&lt;br&gt;
OpenRouter is the pragmatic aggregation plugin. It gives a single endpoint and API key for many models and defaults onboarding to &lt;code&gt;openrouter/auto&lt;/code&gt;, which makes it operationally convenient even if it is not the most opinionated route. Popularity: bundled first-party plugin, proxy metric 359k core repo stars.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;google&lt;/code&gt;&lt;br&gt;&lt;br&gt;
URL: &lt;code&gt;https://docs.openclaw.ai/providers/google&lt;/code&gt;&lt;br&gt;&lt;br&gt;
Google is more than just another text provider in OpenClaw. The plugin also brings image generation, media understanding, and web search via Gemini Grounding. Popularity: bundled first-party plugin, proxy metric 359k core repo stars.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;codex&lt;/code&gt;&lt;br&gt;&lt;br&gt;
URL: &lt;code&gt;https://docs.openclaw.ai/plugins/codex-harness&lt;/code&gt;&lt;br&gt;&lt;br&gt;
The bundled Codex harness is useful when you want the Codex app-server to own the low-level session, thread resume, compaction, and execution path, while OpenClaw still owns channels and visible transcripts. Popularity: bundled first-party plugin, proxy metric 359k core repo stars.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Dev workflows and observability&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;openclaw-codex-app-server&lt;/code&gt;&lt;br&gt;&lt;br&gt;
URL: &lt;code&gt;https://github.com/pwrdrvr/openclaw-codex-app-server&lt;/code&gt;&lt;br&gt;&lt;br&gt;
This is one of the clearest community dev-workflow wins. It binds a chat to a Codex App Server thread and exposes chat-native controls for resume, planning, review, model selection, and compaction. Popularity: about 193 GitHub stars.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;@opik/opik-openclaw&lt;/code&gt;&lt;br&gt;&lt;br&gt;
URL: &lt;code&gt;https://github.com/comet-ml/opik-openclaw&lt;/code&gt;&lt;br&gt;&lt;br&gt;
Opik is the clean observability plugin pick. It exports LLM spans, tool spans, sub-agent spans, usage, and cost metadata to Opik, and it has visible release cadence and public docs. Popularity: about 453 to 459 GitHub stars.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;manifest&lt;/code&gt;&lt;br&gt;&lt;br&gt;
URL: &lt;code&gt;https://github.com/mnfst/manifest/tree/main/packages/openclaw-plugin&lt;/code&gt;&lt;br&gt;&lt;br&gt;
Manifest matters because it combines model routing and observability in one plugin, intercepting requests to score and route them while recording costs and timings. It is one of the bigger public projects in the ecosystem, though it has also had public friction around scanner warnings and onboarding noise. Popularity: about 4.3k GitHub stars.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Voice agents and multi-step workflows&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;voice-call&lt;/code&gt;&lt;br&gt;&lt;br&gt;
URL: &lt;code&gt;https://docs.openclaw.ai/plugins/voice-call&lt;/code&gt;&lt;br&gt;&lt;br&gt;
This is the useful voice plugin, not the flashy one. It supports outbound calls, multi-turn conversations, inbound call policies, and current providers including Twilio, Telnyx, Plivo, and a mock transport. Popularity: bundled first-party plugin, proxy metric 359k core repo stars.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;webhooks&lt;/code&gt;&lt;br&gt;&lt;br&gt;
URL: &lt;code&gt;https://docs.openclaw.ai/plugins/webhooks&lt;/code&gt;&lt;br&gt;&lt;br&gt;
The Webhooks plugin is the most underrated workflow plugin because it lets trusted systems such as Zapier, n8n, CI jobs, or internal services create and drive TaskFlows over authenticated HTTP routes. It is much less glamorous than AI orchestration marketing, but much closer to how teams actually automate work. Popularity: bundled first-party plugin, proxy metric 359k core repo stars.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Lead generation, CRM, and email-calendar automation&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the part of the ecosystem where restraint is healthy. Based on the public packages and repositories I could verify, OpenClaw has promising native plugin experiments for Google Workspace and Google Calendar, and there are early CRM-oriented packages in the wider ecosystem, but the public popularity signals are still very small. &lt;code&gt;tensorfold/openclaw-google-workspace&lt;/code&gt; presented an all-in-one Gmail, Calendar, Drive, Contacts, Tasks, and Sheets plugin but showed 0 GitHub stars. &lt;code&gt;alefsolutions/openclaw-google-calendar&lt;/code&gt; also showed 0 GitHub stars. &lt;code&gt;crm-skills-openclaw&lt;/code&gt; existed publicly with HubSpot and Salesforce direction, but it is a skill-oriented repo rather than a mature native plugin, and it showed about 1 GitHub star. That does not make these projects useless. It makes them early.&lt;/p&gt;

&lt;p&gt;There is also an interesting social-and-growth plugin direction. SendIt exposes publishing, analytics, campaigns, inbox, CRM, and workflow tools through an OpenClaw plugin plus a bundled skill pack. Publicly, though, the repo still showed 0 GitHub stars at crawl time. The honest reading is that this category is promising, but not yet popular enough to call mature.&lt;/p&gt;

&lt;p&gt;So the practical conclusion for lead generation and business automation is mildly unromantic. OpenClaw's strongest plugin-native wins today are still web access, memory, routing, channels, voice, and observability. For CRM-heavy or inbox-heavy workflows, the real-world path is still often a mix of Webhooks, a provider or browser plugin, and skills or API bridges rather than one dominant plugin package. That pattern is visible in the public ecosystem itself, and it maps directly to the plugin and skill stacks described in the &lt;a href="https://www.glukhov.org/ai-systems/openclaw/production-setup/" rel="noopener noreferrer"&gt;OpenClaw production setup guide&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bottom line
&lt;/h2&gt;

&lt;p&gt;The useful OpenClaw plugin ecosystem today is less about novelty than about operational leverage. The boring picks are still the right picks: &lt;code&gt;browser&lt;/code&gt;, &lt;code&gt;firecrawl&lt;/code&gt;, &lt;code&gt;tavily&lt;/code&gt;, &lt;code&gt;memory-lancedb&lt;/code&gt;, &lt;code&gt;memory-wiki&lt;/code&gt;, &lt;code&gt;voice-call&lt;/code&gt;, &lt;code&gt;webhooks&lt;/code&gt;, and the bundled provider plugins for OpenAI, Anthropic, Google, OpenRouter, and Codex. On the community side, &lt;code&gt;lossless-claw&lt;/code&gt;, &lt;code&gt;@opik/opik-openclaw&lt;/code&gt;, &lt;code&gt;openclaw-codex-app-server&lt;/code&gt;, &lt;code&gt;manifest&lt;/code&gt;, and &lt;code&gt;wecom&lt;/code&gt; are the clearest public packages with visible utility and public traction.&lt;/p&gt;

&lt;p&gt;When you later evaluate OpenClaw skills on the same registries, use the same hygiene as for plugins (pin versions, read manifests, treat scanning as directional). See the &lt;a href="https://www.glukhov.org/ai-systems/openclaw/skills/" rel="noopener noreferrer"&gt;OpenClaw skills guide&lt;/a&gt; for per-role stacks and a security checklist. For extension directories, keep workspace plugin roots intentional and use allowlists when you cannot trust every path on disk.&lt;/p&gt;

&lt;p&gt;The opinionated read is this. OpenClaw already has a serious native plugin platform, and extension directories give you predictable places to stage that code. Skills widen what you can publish without always widening what runs with full plugin privileges. The part that deserves trust right now is still the runtime plumbing layer for native plugins, not the long tail of business-ops demos. If you want a useful baseline rather than an aspirational one, that is the line to hold.&lt;/p&gt;

&lt;p&gt;For how these plugin choices map to real user types and production workflows, see &lt;a href="https://www.glukhov.org/ai-systems/openclaw/production-setup/" rel="noopener noreferrer"&gt;OpenClaw production setup patterns&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>selfhosting</category>
      <category>llm</category>
      <category>ai</category>
      <category>openclaw</category>
    </item>
    <item>
      <title>OpenClaw Skills Ecosystem and Practical Production Picks</title>
      <dc:creator>Rost</dc:creator>
      <pubDate>Mon, 20 Apr 2026 09:51:37 +0000</pubDate>
      <link>https://dev.to/rosgluk/openclaw-skills-ecosystem-and-practical-production-picks-2imn</link>
      <guid>https://dev.to/rosgluk/openclaw-skills-ecosystem-and-practical-production-picks-2imn</guid>
      <description>&lt;p&gt;OpenClaw has two extension stories, and they are easy to mix up.&lt;/p&gt;

&lt;p&gt;Plugins extend the runtime. Skills extend the agent's behavior.&lt;/p&gt;

&lt;p&gt;That distinction matters. A plugin adds a new capability surface such as a channel, provider, or tool integration. A skill is usually lighter. It teaches the agent how and when to use existing tools, binaries, APIs, or workflows. In practice, that makes skills the faster moving part of the OpenClaw ecosystem, and also the noisier one.&lt;/p&gt;

&lt;p&gt;This article stays on the ecosystem and selection side. For how skills and plugins combine in practice by user type, see &lt;a href="https://www.glukhov.org/ai-systems/openclaw/production-setup/" rel="noopener noreferrer"&gt;OpenClaw production setup patterns&lt;/a&gt;. The question here is simpler and more useful: which skills are actually worth installing, how do they fit into OpenClaw, and which ones look more like noise than durable tooling.&lt;/p&gt;

&lt;p&gt;Popularity notes below use ClawHub stars and downloads as a rough snapshot on 2026-04-18.&lt;/p&gt;

&lt;h2&gt;
  
  
  What OpenClaw skills really are
&lt;/h2&gt;

&lt;p&gt;The OpenClaw skill model is elegant because it is mostly plain files.&lt;/p&gt;

&lt;p&gt;A typical skill looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;my-skill/
  SKILL.md
  scripts/
  references/
  assets/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At minimum, the skill needs &lt;code&gt;SKILL.md&lt;/code&gt;. That file contains YAML frontmatter and markdown instructions that tell the agent what the skill does, when to use it, and what tools or commands are available.&lt;/p&gt;

&lt;p&gt;A minimal example looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;hello_world&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;A&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;simple&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;skill&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;that&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;says&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;hello"&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;span class="gh"&gt;# Hello World Skill&lt;/span&gt;

Use this skill when the user wants a quick greeting.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The useful part is not the markdown itself. The useful part is how OpenClaw loads and gates skills.&lt;/p&gt;

&lt;p&gt;A skill can be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;bundled with OpenClaw&lt;/li&gt;
&lt;li&gt;installed into a workspace&lt;/li&gt;
&lt;li&gt;shared at user level&lt;/li&gt;
&lt;li&gt;scoped to an agent&lt;/li&gt;
&lt;li&gt;injected by a plugin&lt;/li&gt;
&lt;li&gt;filtered by OS, binaries, environment variables, or config&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last point is why OpenClaw skills feel closer to operational recipes than to prompt snippets. A good skill is not only descriptive. It declares enough metadata that OpenClaw can decide whether it should even be visible.&lt;/p&gt;

&lt;p&gt;In other words, the system is more disciplined than the average public "prompt pack" marketplace.&lt;/p&gt;

&lt;h2&gt;
  
  
  OpenClaw skill locations and structure
&lt;/h2&gt;

&lt;p&gt;OpenClaw uses a precedence model rather than a single global skills folder.&lt;/p&gt;

&lt;p&gt;In practice, the highest value locations are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;&amp;lt;workspace&amp;gt;/skills&lt;/code&gt; for project specific overrides&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;&amp;lt;workspace&amp;gt;/.agents/skills&lt;/code&gt; for project agent skills&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;~/.agents/skills&lt;/code&gt; for personal agent skills&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;~/.openclaw/skills&lt;/code&gt; for shared local skills&lt;/li&gt;
&lt;li&gt;bundled skills shipped with the install&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That layout is one of the better design decisions in OpenClaw. It makes skills overrideable without editing the upstream install, and it keeps local customization from turning into a dirty fork.&lt;/p&gt;

&lt;p&gt;It also means skill visibility and skill location are separate concerns.&lt;/p&gt;

&lt;p&gt;A skill can exist locally and still be blocked from a given agent. That happens through skill allowlists in &lt;code&gt;agents.defaults.skills&lt;/code&gt; and &lt;code&gt;agents.list[].skills&lt;/code&gt;. For production, that separation is more important than the marketplace itself. It is what stops every agent from receiving every possible workflow.&lt;/p&gt;

&lt;p&gt;There are also a few frontmatter flags worth remembering:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;user-invocable&lt;/code&gt; exposes a slash command&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;disable-model-invocation&lt;/code&gt; keeps the skill out of the model prompt while still allowing explicit invocation&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;command-dispatch&lt;/code&gt; and &lt;code&gt;command-tool&lt;/code&gt; can bypass model reasoning and call a tool directly&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;metadata.openclaw.requires.*&lt;/code&gt; can gate a skill on binaries, env vars, OS, or config&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is enough structure to make skills powerful, but also enough rope to create fragile packages if the metadata is sloppy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to get OpenClaw skills
&lt;/h2&gt;

&lt;p&gt;For practical use, there are three real sources.&lt;/p&gt;

&lt;h3&gt;
  
  
  ClawHub
&lt;/h3&gt;

&lt;p&gt;ClawHub is the official public registry for OpenClaw skills and plugins. It is the default place to search, install, update, inspect versions, and see lightweight community signals such as stars and downloads.&lt;/p&gt;

&lt;p&gt;If you only pick one source, use ClawHub.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bundled skills
&lt;/h3&gt;

&lt;p&gt;OpenClaw ships with bundled skills inside the install. These are lower friction, but the list is naturally smaller than the public registry.&lt;/p&gt;

&lt;p&gt;Bundled skills are the closest thing the ecosystem has to a supported baseline.&lt;/p&gt;

&lt;h3&gt;
  
  
  Local and Git based skills
&lt;/h3&gt;

&lt;p&gt;You can also keep skills in your own workspace or user folders, or pull them from public repositories.&lt;/p&gt;

&lt;p&gt;This is useful for private skills, experiments, and local overrides.&lt;/p&gt;

&lt;p&gt;There is also a public archived repository of registry skills on GitHub. It is useful as an audit trail, not as the first place to install from. Treat it as a historical dump and inspection surface, not as a curated store.&lt;/p&gt;

&lt;p&gt;Community discovery layers such as awesome lists and filtered indexes are now part of the ecosystem as well. That is a signal in itself. Once a marketplace gets large enough, secondary curation becomes necessary.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to install, update, and remove skills
&lt;/h2&gt;

&lt;p&gt;The normal install flow is through the OpenClaw CLI.&lt;/p&gt;

&lt;h3&gt;
  
  
  Search
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openclaw skills search &lt;span class="s2"&gt;"calendar"&lt;/span&gt;
openclaw skills search &lt;span class="s2"&gt;"github"&lt;/span&gt;
openclaw skills search &lt;span class="nt"&gt;--limit&lt;/span&gt; 20 &lt;span class="nt"&gt;--json&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Install
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openclaw skills &lt;span class="nb"&gt;install&lt;/span&gt; &amp;lt;skill-slug&amp;gt;
openclaw skills &lt;span class="nb"&gt;install&lt;/span&gt; &amp;lt;skill-slug&amp;gt; &lt;span class="nt"&gt;--version&lt;/span&gt; &amp;lt;version&amp;gt;
openclaw skills &lt;span class="nb"&gt;install&lt;/span&gt; &amp;lt;skill-slug&amp;gt; &lt;span class="nt"&gt;--force&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By default, &lt;code&gt;openclaw skills install&lt;/code&gt; places the skill into the active workspace &lt;code&gt;skills/&lt;/code&gt; directory.&lt;/p&gt;

&lt;h3&gt;
  
  
  Update
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openclaw skills update &amp;lt;skill-slug&amp;gt;
openclaw skills update &lt;span class="nt"&gt;--all&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Inspect and validate
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openclaw skills list
openclaw skills list &lt;span class="nt"&gt;--eligible&lt;/span&gt;
openclaw skills info &amp;lt;name&amp;gt;
openclaw skills check
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Install with the dedicated ClawHub CLI
&lt;/h3&gt;

&lt;p&gt;If you publish skills, sync local folders, or want registry specific workflows, use the separate &lt;code&gt;clawhub&lt;/code&gt; CLI.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm i &lt;span class="nt"&gt;-g&lt;/span&gt; clawhub

clawhub search &lt;span class="s2"&gt;"research"&lt;/span&gt;
clawhub &lt;span class="nb"&gt;install&lt;/span&gt; &amp;lt;skill-slug&amp;gt;
clawhub update &lt;span class="nt"&gt;--all&lt;/span&gt;
clawhub skill publish ./my-skill &lt;span class="nt"&gt;--slug&lt;/span&gt; my-skill &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"My Skill"&lt;/span&gt; &lt;span class="nt"&gt;--version&lt;/span&gt; 1.0.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The dedicated CLI writes a &lt;code&gt;.clawhub/lock.json&lt;/code&gt; file in the working directory, which is useful for tracking what came from the registry.&lt;/p&gt;

&lt;h3&gt;
  
  
  Removal
&lt;/h3&gt;

&lt;p&gt;This part is less polished than installation.&lt;/p&gt;

&lt;p&gt;OpenClaw documents install and update flows for skills, but not a dedicated &lt;code&gt;openclaw skills uninstall&lt;/code&gt; command. In practice, removal is filesystem based.&lt;/p&gt;

&lt;p&gt;If a skill was installed into the workspace, remove its folder from &lt;code&gt;&amp;lt;workspace&amp;gt;/skills&lt;/code&gt;, then start a new session.&lt;/p&gt;

&lt;p&gt;If you want the skill to stay present but not be usable by a given agent, use skill allowlists instead of deletion.&lt;/p&gt;

&lt;p&gt;That sounds a little manual because it is. The skill system is clean. The lifecycle UX is still catching up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Maturity, reliability, community, and support
&lt;/h2&gt;

&lt;p&gt;The skill system is mature enough to be real, but not mature enough to be calm.&lt;/p&gt;

&lt;p&gt;That is the shortest honest summary.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is mature
&lt;/h3&gt;

&lt;p&gt;The underlying model is solid.&lt;/p&gt;

&lt;p&gt;Skills are plain files, easy to inspect, easy to override, easy to version, and flexible enough to express both tiny instruction packs and fairly serious task helpers. OpenClaw also separates visibility, precedence, and runtime gating in a way that feels intentionally designed rather than bolted on.&lt;/p&gt;

&lt;p&gt;The community signal is also real. OpenClaw itself is one of the most visible open source AI agent projects right now, and the skill ecosystem is large enough that third party curation has already appeared.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is not mature
&lt;/h3&gt;

&lt;p&gt;Registry quality is uneven.&lt;/p&gt;

&lt;p&gt;The interesting issue is not whether a skill can work. Many do. The issue is whether the packaging, metadata, secret handling, and trust story are coherent.&lt;/p&gt;

&lt;p&gt;A good OpenClaw skill is narrow, boring, and inspectable.&lt;/p&gt;

&lt;p&gt;A weak OpenClaw skill usually has one or more of these problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;metadata that does not match what the skill actually needs&lt;/li&gt;
&lt;li&gt;hidden or undocumented environment variables&lt;/li&gt;
&lt;li&gt;third party taps or installers with thin provenance&lt;/li&gt;
&lt;li&gt;broad account access for a narrow task&lt;/li&gt;
&lt;li&gt;hooks that quietly become default behavior&lt;/li&gt;
&lt;li&gt;an impressive pitch with very little durable workflow value&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is why "most downloaded" is not the same thing as "production ready".&lt;/p&gt;

&lt;h3&gt;
  
  
  Support reality
&lt;/h3&gt;

&lt;p&gt;Support comes from a mix of places:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;official docs&lt;/li&gt;
&lt;li&gt;ClawHub metadata and scan pages&lt;/li&gt;
&lt;li&gt;GitHub issues and repository history&lt;/li&gt;
&lt;li&gt;community comments and curation lists&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is enough for active operators. It is not the same as enterprise support.&lt;/p&gt;

&lt;p&gt;If you need predictable ownership and response times, the skill ecosystem still feels more open source registry than platform contract.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security concerns are not optional
&lt;/h2&gt;

&lt;p&gt;OpenClaw is powerful because it can act.&lt;/p&gt;

&lt;p&gt;That also means skills should be treated as code, not decoration.&lt;/p&gt;

&lt;p&gt;The official security posture already hints at the correct mental model. Run the gateway on a dedicated machine, VM, or container. Use a dedicated OS user. Keep personal accounts and browser profiles away from that runtime. Restrict high risk tools. Treat links, attachments, and pasted instructions as hostile by default.&lt;/p&gt;

&lt;p&gt;That guidance becomes more important, not less, once skills enter the picture.&lt;/p&gt;

&lt;p&gt;The ClawHub moderation story has improved, but it is still fundamentally a public registry. Skills can be reported, hidden, deleted, and scanned. Publishing now has some basic controls. But the high level lesson from recent incidents is obvious: a public skill registry attracts malware quickly.&lt;/p&gt;

&lt;p&gt;The right filter is simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;instruction only skills are usually lower risk&lt;/li&gt;
&lt;li&gt;small helper scripts can be fine if metadata and provenance are clean&lt;/li&gt;
&lt;li&gt;hooks deserve extra scrutiny&lt;/li&gt;
&lt;li&gt;skills that touch sensitive accounts need the highest bar&lt;/li&gt;
&lt;li&gt;any scan flag should matter more than social hype&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Popularity is not a trust signal. At best, it is a hint that a skill solved a real problem for many people.&lt;/p&gt;

&lt;h2&gt;
  
  
  The most useful OpenClaw skills right now
&lt;/h2&gt;

&lt;p&gt;The most useful skills are not the flashiest ones. They are the ones that make repeated workflows cheaper, clearer, or safer.&lt;/p&gt;

&lt;p&gt;My filter here is opinionated:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;narrow scope beats broad promise&lt;/li&gt;
&lt;li&gt;inspectable beats magical&lt;/li&gt;
&lt;li&gt;local or transparent beats opaque proxying&lt;/li&gt;
&lt;li&gt;workflow value beats novelty&lt;/li&gt;
&lt;li&gt;clean packaging beats vibes&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Safety and self correction
&lt;/h3&gt;

&lt;p&gt;These are the least glamorous skills in the ecosystem, which is exactly why they matter.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Skill&lt;/th&gt;
&lt;th&gt;URL&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;th&gt;Why it is useful&lt;/th&gt;
&lt;th&gt;Popularity&lt;/th&gt;
&lt;th&gt;Scan note&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;self-improving-agent&lt;/td&gt;
&lt;td&gt;&lt;a href="https://clawhub.ai/pskoett/self-improving-agent" rel="noopener noreferrer"&gt;https://clawhub.ai/pskoett/self-improving-agent&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Captures learnings, errors, and corrections for future runs&lt;/td&gt;
&lt;td&gt;One of the few skills that improves repeat work instead of adding another endpoint&lt;/td&gt;
&lt;td&gt;3.2k stars, 396k downloads&lt;/td&gt;
&lt;td&gt;benign&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Skill Vetter 1.0.0&lt;/td&gt;
&lt;td&gt;&lt;a href="https://clawhub.ai/fedrov2025/skill-vetter-1-0-0" rel="noopener noreferrer"&gt;https://clawhub.ai/fedrov2025/skill-vetter-1-0-0&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Reviews other skills for red flags before install&lt;/td&gt;
&lt;td&gt;The skill ecosystem needed this very early, which says a lot about the ecosystem&lt;/td&gt;
&lt;td&gt;9 stars, 7.3k downloads&lt;/td&gt;
&lt;td&gt;benign&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The first one is popular for a reason. It is not a gimmick. It creates a feedback loop around failure, which is one of the few things that consistently pays off in agent systems.&lt;/p&gt;

&lt;p&gt;The second one is not popular in absolute terms, but it is one of the most sensible installs you can add if you plan to browse ClawHub regularly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Search and research
&lt;/h3&gt;

&lt;p&gt;Search skills are where OpenClaw gets genuinely useful, but also where packaging quality varies a lot.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Skill&lt;/th&gt;
&lt;th&gt;URL&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;th&gt;Why it is useful&lt;/th&gt;
&lt;th&gt;Popularity&lt;/th&gt;
&lt;th&gt;Scan note&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Multi Search Engine&lt;/td&gt;
&lt;td&gt;&lt;a href="https://clawhub.ai/gpyangyoujun/multi-search-engine" rel="noopener noreferrer"&gt;https://clawhub.ai/gpyangyoujun/multi-search-engine&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Aggregates 16 search engines with operators and time filters&lt;/td&gt;
&lt;td&gt;Better than single engine skills when you need broad recall&lt;/td&gt;
&lt;td&gt;566 stars, 121k downloads&lt;/td&gt;
&lt;td&gt;benign&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tavily Search&lt;/td&gt;
&lt;td&gt;&lt;a href="https://clawhub.ai/matthew77/liang-tavily-search" rel="noopener noreferrer"&gt;https://clawhub.ai/matthew77/liang-tavily-search&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Tavily backed web search with snippets and metadata&lt;/td&gt;
&lt;td&gt;Clean, narrow, and easy to reason about&lt;/td&gt;
&lt;td&gt;92 stars, 36.2k downloads&lt;/td&gt;
&lt;td&gt;benign&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Academic Deep Research&lt;/td&gt;
&lt;td&gt;&lt;a href="https://clawhub.ai/kesslerio/academic-deep-research" rel="noopener noreferrer"&gt;https://clawhub.ai/kesslerio/academic-deep-research&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Forces multi cycle research with explicit method&lt;/td&gt;
&lt;td&gt;Good when you want structure, not just a quick answer&lt;/td&gt;
&lt;td&gt;53 stars, 17.2k downloads&lt;/td&gt;
&lt;td&gt;benign&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The strongest pattern here is that method often beats breadth.&lt;/p&gt;

&lt;p&gt;Multi Search Engine is the broad utility pick. Tavily Search is the cleaner service backed pick. Academic Deep Research is the process pick. None of them are flashy. All of them can be useful.&lt;/p&gt;

&lt;h3&gt;
  
  
  Developer workflows
&lt;/h3&gt;

&lt;p&gt;This is the most obviously valuable category for technical readers.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Skill&lt;/th&gt;
&lt;th&gt;URL&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;th&gt;Why it is useful&lt;/th&gt;
&lt;th&gt;Popularity&lt;/th&gt;
&lt;th&gt;Scan note&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Github&lt;/td&gt;
&lt;td&gt;&lt;a href="https://clawhub.ai/steipete/github" rel="noopener noreferrer"&gt;https://clawhub.ai/steipete/github&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Uses the &lt;code&gt;gh&lt;/code&gt; CLI for issues, PRs, runs, and API calls&lt;/td&gt;
&lt;td&gt;One of the cleanest examples of a skill that maps directly to a real CLI&lt;/td&gt;
&lt;td&gt;514 stars, 159k downloads&lt;/td&gt;
&lt;td&gt;benign&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Agent Browser&lt;/td&gt;
&lt;td&gt;&lt;a href="https://clawhub.ai/matrixy/agent-browser-clawdbot" rel="noopener noreferrer"&gt;https://clawhub.ai/matrixy/agent-browser-clawdbot&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Headless browser automation with snapshots and refs&lt;/td&gt;
&lt;td&gt;Useful for tests, admin flows, and web tasks that are too awkward for plain fetch&lt;/td&gt;
&lt;td&gt;323 stars, 90.1k downloads&lt;/td&gt;
&lt;td&gt;benign&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Opencode-controller&lt;/td&gt;
&lt;td&gt;&lt;a href="https://clawhub.ai/karatla/opencode-controller" rel="noopener noreferrer"&gt;https://clawhub.ai/karatla/opencode-controller&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Controls Opencode sessions, agents, and models&lt;/td&gt;
&lt;td&gt;Practical if Opencode is already part of your workflow&lt;/td&gt;
&lt;td&gt;72 stars, 17.9k downloads&lt;/td&gt;
&lt;td&gt;benign&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The GitHub skill is the kind of skill the ecosystem should have more of. It is boring, direct, and tied to a tool developers already know.&lt;/p&gt;

&lt;p&gt;Agent Browser is more powerful, but also deserves more care. Browser state files, cookies, and page context are real data surfaces. That does not make the skill bad. It makes it operational.&lt;/p&gt;

&lt;h3&gt;
  
  
  Memory and knowledge
&lt;/h3&gt;

&lt;p&gt;This category is more valuable than it looks at first glance.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Skill&lt;/th&gt;
&lt;th&gt;URL&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;th&gt;Why it is useful&lt;/th&gt;
&lt;th&gt;Popularity&lt;/th&gt;
&lt;th&gt;Scan note&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ontology&lt;/td&gt;
&lt;td&gt;&lt;a href="https://clawhub.ai/oswalpalash/ontology" rel="noopener noreferrer"&gt;https://clawhub.ai/oswalpalash/ontology&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Typed knowledge graph for local structured memory&lt;/td&gt;
&lt;td&gt;One of the strongest memory oriented skills I found&lt;/td&gt;
&lt;td&gt;539 stars, 166k downloads&lt;/td&gt;
&lt;td&gt;benign&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Academic Deep Research&lt;/td&gt;
&lt;td&gt;&lt;a href="https://clawhub.ai/kesslerio/academic-deep-research" rel="noopener noreferrer"&gt;https://clawhub.ai/kesslerio/academic-deep-research&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Research workflow with explicit evidence handling&lt;/td&gt;
&lt;td&gt;Useful as a temporary method layer when memory quality matters&lt;/td&gt;
&lt;td&gt;53 stars, 17.2k downloads&lt;/td&gt;
&lt;td&gt;benign&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The ontology skill stands out because it treats memory as structure rather than as note accumulation. That is a stronger long term direction for agent systems than endlessly appending summaries.&lt;/p&gt;

&lt;h3&gt;
  
  
  Workspace and personal productivity
&lt;/h3&gt;

&lt;p&gt;This is the most uneven category. It contains genuinely useful skills, but also some of the most obvious metadata mismatches.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Skill&lt;/th&gt;
&lt;th&gt;URL&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;th&gt;Why it is useful&lt;/th&gt;
&lt;th&gt;Popularity&lt;/th&gt;
&lt;th&gt;Scan note&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Gog&lt;/td&gt;
&lt;td&gt;&lt;a href="https://clawhub.ai/steipete/gog" rel="noopener noreferrer"&gt;https://clawhub.ai/steipete/gog&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Google Workspace CLI for Gmail, Calendar, Drive, Sheets, Docs&lt;/td&gt;
&lt;td&gt;Very practical if your work already lives in Google Workspace&lt;/td&gt;
&lt;td&gt;839 stars, 157k downloads&lt;/td&gt;
&lt;td&gt;suspicious&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Notion&lt;/td&gt;
&lt;td&gt;&lt;a href="https://clawhub.ai/steipete/notion" rel="noopener noreferrer"&gt;https://clawhub.ai/steipete/notion&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Notion API helper for pages, blocks, and databases&lt;/td&gt;
&lt;td&gt;Useful in theory and often useful in practice, but packaging details matter&lt;/td&gt;
&lt;td&gt;229 stars, 77.4k downloads&lt;/td&gt;
&lt;td&gt;suspicious&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Openai Whisper&lt;/td&gt;
&lt;td&gt;&lt;a href="https://clawhub.ai/steipete/openai-whisper" rel="noopener noreferrer"&gt;https://clawhub.ai/steipete/openai-whisper&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Local Whisper CLI transcription&lt;/td&gt;
&lt;td&gt;One of the best examples of a narrow, useful local skill&lt;/td&gt;
&lt;td&gt;274 stars, 70k downloads&lt;/td&gt;
&lt;td&gt;benign&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This is where the ecosystem gets interesting.&lt;/p&gt;

&lt;p&gt;Gog is clearly useful. It is also a good example of why utility and trust are separate questions. The current scan notes point out metadata mismatches around binaries and credentials. That does not automatically make it malicious. It does make it a skill to inspect before granting account access.&lt;/p&gt;

&lt;p&gt;Notion sits in the same category. Good workflow value. Messier packaging story.&lt;/p&gt;

&lt;p&gt;Openai Whisper is the opposite. It is limited, local, and refreshingly straightforward.&lt;/p&gt;

&lt;h2&gt;
  
  
  The skills I would not rush to install
&lt;/h2&gt;

&lt;p&gt;Some skills are popular for understandable reasons and still do not make my first pass list.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Skill&lt;/th&gt;
&lt;th&gt;URL&lt;/th&gt;
&lt;th&gt;Why I would wait&lt;/th&gt;
&lt;th&gt;Popularity&lt;/th&gt;
&lt;th&gt;Scan note&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Desktop Control&lt;/td&gt;
&lt;td&gt;&lt;a href="https://clawhub.ai/matagul/desktop-control" rel="noopener noreferrer"&gt;https://clawhub.ai/matagul/desktop-control&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Powerful enough to matter, but current scan status is a red flag and the capability is sensitive by design&lt;/td&gt;
&lt;td&gt;299 stars, 47.7k downloads&lt;/td&gt;
&lt;td&gt;suspicious&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Baidu web search&lt;/td&gt;
&lt;td&gt;&lt;a href="https://clawhub.ai/ide-rea/baidu-search" rel="noopener noreferrer"&gt;https://clawhub.ai/ide-rea/baidu-search&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Good idea, but undocumented env vars and metadata gaps are exactly the kind of sloppiness that should slow you down&lt;/td&gt;
&lt;td&gt;203 stars, 79.2k downloads&lt;/td&gt;
&lt;td&gt;suspicious&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Obsidian&lt;/td&gt;
&lt;td&gt;&lt;a href="https://clawhub.ai/steipete/obsidian" rel="noopener noreferrer"&gt;https://clawhub.ai/steipete/obsidian&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;High utility, but current scan notes call out mismatched metadata and undeclared file access&lt;/td&gt;
&lt;td&gt;333 stars, 82.5k downloads&lt;/td&gt;
&lt;td&gt;suspicious&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That is the larger pattern in one table.&lt;/p&gt;

&lt;p&gt;High download counts do not erase packaging problems.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real shape of the OpenClaw skills ecosystem
&lt;/h2&gt;

&lt;p&gt;The OpenClaw skills ecosystem is already big enough to be useful and already noisy enough to need curation.&lt;/p&gt;

&lt;p&gt;That is usually the moment an ecosystem becomes real.&lt;/p&gt;

&lt;p&gt;The good news is that the underlying skill format is strong. Skills are inspectable. Overrides are clean. Precedence is sensible. Gating is practical. ClawHub provides versioning, discovery, stars, downloads, comments, and basic moderation.&lt;/p&gt;

&lt;p&gt;The bad news is that public registries move faster than trust models.&lt;/p&gt;

&lt;p&gt;If you want the short opinionated take, it is this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the skill system is better than the average AI marketplace&lt;/li&gt;
&lt;li&gt;the registry is more useful than safe by default&lt;/li&gt;
&lt;li&gt;the best skills are small, specific, and operationally boring&lt;/li&gt;
&lt;li&gt;suspicious metadata is not a cosmetic issue&lt;/li&gt;
&lt;li&gt;"popular" should never outrank "inspectable"&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Final take
&lt;/h2&gt;

&lt;p&gt;If I were trimming OpenClaw skills down to the set that looks most durable right now, I would start with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;self-improving-agent&lt;/li&gt;
&lt;li&gt;Skill Vetter&lt;/li&gt;
&lt;li&gt;Github&lt;/li&gt;
&lt;li&gt;Multi Search Engine&lt;/li&gt;
&lt;li&gt;Tavily Search&lt;/li&gt;
&lt;li&gt;Academic Deep Research&lt;/li&gt;
&lt;li&gt;ontology&lt;/li&gt;
&lt;li&gt;Openai Whisper&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then I would consider Gog and Notion only after a manual review of current metadata, source, and secret handling.&lt;/p&gt;

&lt;p&gt;That is probably the right framing for the entire OpenClaw skills ecosystem in 2026.&lt;/p&gt;

&lt;p&gt;The good part is already very good.&lt;/p&gt;

&lt;p&gt;The safe part still requires an adult in the room.&lt;/p&gt;




&lt;p&gt;For how skills combine with plugins in real deployments by user type, see &lt;a href="https://www.glukhov.org/ai-systems/openclaw/production-setup/" rel="noopener noreferrer"&gt;OpenClaw production setup patterns&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For the plugin layer those skills depend on, see &lt;a href="https://www.glukhov.org/ai-systems/openclaw/plugins/" rel="noopener noreferrer"&gt;OpenClaw plugins guide&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>selfhosting</category>
      <category>openclaw</category>
      <category>llm</category>
      <category>ai</category>
    </item>
    <item>
      <title>OpenClaw Production Setup Patterns with Plugins and Skills</title>
      <dc:creator>Rost</dc:creator>
      <pubDate>Mon, 20 Apr 2026 09:51:33 +0000</pubDate>
      <link>https://dev.to/rosgluk/openclaw-production-setup-patterns-with-plugins-and-skills-1jfj</link>
      <guid>https://dev.to/rosgluk/openclaw-production-setup-patterns-with-plugins-and-skills-1jfj</guid>
      <description>&lt;p&gt;OpenClaw looks simple in demos.&lt;br&gt;
In production, it becomes a system.&lt;/p&gt;

&lt;p&gt;The real complexity is not in prompts or models. It is in how plugins and skills interact to manage state, integrate systems, and execute workflows over time.&lt;/p&gt;

&lt;p&gt;A useful mental model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Plugins = capabilities&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
APIs, memory, tools, integrations&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Skills = behavior&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
How the agent uses those capabilities in structured ways&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Production systems fail when these two are mixed without boundaries.&lt;/p&gt;

&lt;p&gt;They become reliable when both are mapped to real user needs.&lt;/p&gt;




&lt;h2&gt;
  
  
  How to think about production setup
&lt;/h2&gt;

&lt;p&gt;Most teams ask what plugins or skills they should install.&lt;/p&gt;

&lt;p&gt;That is the wrong starting point.&lt;/p&gt;

&lt;p&gt;A better question is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Who is this system for, and what work are they trying to complete?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Each user type creates a different architecture:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;developers need control and traceability&lt;/li&gt;
&lt;li&gt;automation users need triggers and determinism&lt;/li&gt;
&lt;li&gt;researchers need memory and retrieval&lt;/li&gt;
&lt;li&gt;support teams need continuity and communication&lt;/li&gt;
&lt;li&gt;growth teams need pipelines and data flow&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Plugins enable these systems.&lt;br&gt;&lt;br&gt;
Skills make them usable.&lt;/p&gt;

&lt;p&gt;The combination of both, tailored to a real user profile, is what separates a production system from a demo.&lt;/p&gt;




&lt;h2&gt;
  
  
  Installation and lifecycle note
&lt;/h2&gt;

&lt;p&gt;This article focuses on architecture patterns and user-specific configurations.&lt;/p&gt;

&lt;p&gt;For full installation and lifecycle details see:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.glukhov.org/ai-systems/openclaw/plugins/" rel="noopener noreferrer"&gt;OpenClaw plugins guide&lt;/a&gt; — plugin installation, extension directories, CLI lifecycle, and mature picks&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.glukhov.org/ai-systems/openclaw/skills/" rel="noopener noreferrer"&gt;OpenClaw skills guide&lt;/a&gt; — ClawHub discovery, install and removal flows, security tradeoffs, and per-role stacks&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.glukhov.org/ai-systems/openclaw/quickstart/" rel="noopener noreferrer"&gt;OpenClaw quickstart&lt;/a&gt; — Docker-based installation with Ollama GPU or Claude&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In production, both plugins and skills should be treated as dependencies with version control, review, and rollback strategies.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. The Developer Workflow User
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Profile
&lt;/h3&gt;

&lt;p&gt;This user treats OpenClaw as an execution layer for development workflows.&lt;/p&gt;

&lt;p&gt;Not just code generation, but:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;debugging&lt;/li&gt;
&lt;li&gt;iteration&lt;/li&gt;
&lt;li&gt;multi-step reasoning&lt;/li&gt;
&lt;li&gt;repository interaction&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The system is expected to remember decisions, track changes, and make its reasoning visible.&lt;/p&gt;

&lt;h3&gt;
  
  
  Core needs
&lt;/h3&gt;

&lt;p&gt;The key requirement is continuity and visibility.&lt;/p&gt;

&lt;p&gt;Developers need to understand:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;what the system did&lt;/li&gt;
&lt;li&gt;why it did it&lt;/li&gt;
&lt;li&gt;how to reproduce or fix it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without structured memory, every session starts from scratch. Without observability, failures are invisible and expensive to diagnose.&lt;/p&gt;




&lt;h3&gt;
  
  
  Typical OpenClaw Plugin Set
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;model providers&lt;br&gt;&lt;br&gt;
openai, anthropic, openrouter for fallback routing&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;memory and context&lt;br&gt;&lt;br&gt;
memory lancedb, lossless claw&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;dev workflow&lt;br&gt;&lt;br&gt;
codex app server, codex harness&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;observability&lt;br&gt;&lt;br&gt;
opik openclaw, manifest&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Why this helps
&lt;/h4&gt;

&lt;p&gt;Plugins transform OpenClaw into a controlled execution environment.&lt;/p&gt;

&lt;p&gt;Memory lancedb and lossless claw preserve intent across iterations, so the system does not reset its understanding every few turns. Lossless context plugins are especially valuable here because they preserve intent rather than raw tokens.&lt;/p&gt;

&lt;p&gt;Codex plugins move the agent from passive assistant to active participant. They enable real execution, validation, and iteration on code rather than static responses.&lt;/p&gt;

&lt;p&gt;Observability completes the picture. It answers what happened, which is often more important than the output itself. Without this layer, the system feels intelligent but remains unreliable in practice.&lt;/p&gt;




&lt;h3&gt;
  
  
  Typical OpenClaw Skill Set
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Skill&lt;/th&gt;
&lt;th&gt;Link&lt;/th&gt;
&lt;th&gt;Why it helps&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;github&lt;/td&gt;
&lt;td&gt;&lt;a href="https://clawhub.ai/steipete/github" rel="noopener noreferrer"&gt;clawhub.ai/steipete/github&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Best day-to-day control plane for issues, PRs, CI status, and &lt;code&gt;gh&lt;/code&gt; API queries. Instruction-only and low risk. 517 stars, 159k downloads.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tmux&lt;/td&gt;
&lt;td&gt;&lt;a href="https://clawhub.ai/steipete/tmux" rel="noopener noreferrer"&gt;clawhub.ai/steipete/tmux&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Keeps long-running builds, test servers, and agent-driven shells from collapsing into one fragile terminal. 38 stars, 22.5k downloads.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;session-logs&lt;/td&gt;
&lt;td&gt;&lt;a href="https://clawhub.ai/guogang1024/session-logs" rel="noopener noreferrer"&gt;clawhub.ai/guogang1024/session-logs&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Turns prior agent sessions into searchable operational memory. Answers "what did the agent actually do yesterday". 22 stars, 30.9k downloads.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;model-usage&lt;/td&gt;
&lt;td&gt;&lt;a href="https://clawhub.ai/steipete/model-usage" rel="noopener noreferrer"&gt;clawhub.ai/steipete/model-usage&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Local model cost breakdowns by model rather than a vague monthly bill. 101 stars, 32k downloads.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;nano-pdf&lt;/td&gt;
&lt;td&gt;&lt;a href="https://clawhub.ai/steipete/nano-pdf" rel="noopener noreferrer"&gt;clawhub.ai/steipete/nano-pdf&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Handles release notes, partner decks, and PDF patching without context switching. 220 stars, 91.5k downloads.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;openclaw-token-optimizer&lt;/td&gt;
&lt;td&gt;&lt;a href="https://clawhub.ai/asif2bd/openclaw-token-optimizer" rel="noopener noreferrer"&gt;clawhub.ai/asif2bd/openclaw-token-optimizer&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Workspace-level token and cost hygiene when usage creeps up from overpowered defaults. 28 stars, 9.4k downloads.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;openclaw-skill-vetter&lt;/td&gt;
&lt;td&gt;&lt;a href="https://clawhub.ai/donovanpankratz-del/openclaw-skill-vetter" rel="noopener noreferrer"&gt;clawhub.ai/donovanpankratz-del/openclaw-skill-vetter&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Pre-install review checklist for suspicious community skills and risky bundles. 24 stars, 17.4k downloads.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h4&gt;
  
  
  Why this helps
&lt;/h4&gt;

&lt;p&gt;Skills define how developers actually work with the system.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;github skill enables real repository workflows instead of manual copy-paste&lt;/li&gt;
&lt;li&gt;tmux allows long-running or parallel agent tasks without session loss&lt;/li&gt;
&lt;li&gt;session-logs provide operational memory beyond the chat window&lt;/li&gt;
&lt;li&gt;model-usage and token-optimizer expose cost and performance patterns&lt;/li&gt;
&lt;li&gt;skill-vetter adds package-review discipline before any community install&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Plugins give capability. Skills turn that into repeatable engineering workflows.&lt;/p&gt;




&lt;h3&gt;
  
  
  How plugins and skills together serve the developer
&lt;/h3&gt;

&lt;p&gt;The plugin layer provides the raw infrastructure: persistent memory, code execution, and observability.&lt;/p&gt;

&lt;p&gt;The skill layer structures how a developer actually interacts with that infrastructure day to day.&lt;/p&gt;

&lt;p&gt;A developer with codex plugins but no github skill has execution power without workflow integration. A developer with session-logs but no memory plugin has audit trails without cross-session context.&lt;/p&gt;

&lt;p&gt;The combination is what makes the system feel like a reliable collaborator rather than an unpredictable assistant.&lt;/p&gt;

&lt;p&gt;For more on skill selection and security review for this profile see the &lt;a href="https://www.glukhov.org/ai-systems/openclaw/skills/" rel="noopener noreferrer"&gt;OpenClaw skills guide&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  OpenClaw Skill and Plugin Install for Developer Workflow
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Plugins — capabilities layer&lt;/span&gt;
openclaw plugins &lt;span class="nb"&gt;install &lt;/span&gt;memory-lancedb             &lt;span class="c"&gt;# persistent long-term memory with vector recall&lt;/span&gt;
openclaw plugins &lt;span class="nb"&gt;install &lt;/span&gt;lossless-claw              &lt;span class="c"&gt;# lossless context compression, preserves intent not tokens&lt;/span&gt;
openclaw plugins &lt;span class="nb"&gt;install &lt;/span&gt;openclaw-codex-app-server  &lt;span class="c"&gt;# code execution harness, resume, planning, and model selection&lt;/span&gt;
openclaw plugins &lt;span class="nb"&gt;install&lt;/span&gt; @opik/opik-openclaw        &lt;span class="c"&gt;# LLM observability: spans, tool calls, usage, and cost&lt;/span&gt;

&lt;span class="c"&gt;# Skills — behavior layer&lt;/span&gt;
openclaw skills &lt;span class="nb"&gt;install &lt;/span&gt;github                      &lt;span class="c"&gt;# PR, issue, CI status, and gh API workflows&lt;/span&gt;
openclaw skills &lt;span class="nb"&gt;install &lt;/span&gt;tmux                        &lt;span class="c"&gt;# persistent terminal sessions for long-running tasks&lt;/span&gt;
openclaw skills &lt;span class="nb"&gt;install &lt;/span&gt;session-logs                &lt;span class="c"&gt;# searchable agent session history across days&lt;/span&gt;
openclaw skills &lt;span class="nb"&gt;install &lt;/span&gt;model-usage                 &lt;span class="c"&gt;# per-model cost breakdown from session logs&lt;/span&gt;
openclaw skills &lt;span class="nb"&gt;install &lt;/span&gt;nano-pdf                    &lt;span class="c"&gt;# PDF editing, patching, and release note handling&lt;/span&gt;
openclaw skills &lt;span class="nb"&gt;install &lt;/span&gt;openclaw-token-optimizer    &lt;span class="c"&gt;# workspace-level token and cost hygiene&lt;/span&gt;
openclaw skills &lt;span class="nb"&gt;install &lt;/span&gt;openclaw-skill-vetter       &lt;span class="c"&gt;# pre-install review checklist before adding community skills&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  2. The Automation and Ops User
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Profile
&lt;/h3&gt;

&lt;p&gt;This user is not chatting. They are orchestrating.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;workflows&lt;/li&gt;
&lt;li&gt;triggers&lt;/li&gt;
&lt;li&gt;pipelines&lt;/li&gt;
&lt;li&gt;system integrations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For this profile, OpenClaw becomes part of infrastructure, not a UI. The system is expected to react to events and coordinate workflows across systems without human intervention at each step.&lt;/p&gt;

&lt;h3&gt;
  
  
  Core needs
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;deterministic execution&lt;/li&gt;
&lt;li&gt;external triggers&lt;/li&gt;
&lt;li&gt;reliability under failure&lt;/li&gt;
&lt;li&gt;integration with existing systems&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The focus shifts from intelligence to predictability. Automation workflows must be repeatable, externally triggered, and easy to integrate into existing infrastructure.&lt;/p&gt;




&lt;h3&gt;
  
  
  Typical OpenClaw Plugin Set
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;workflow and triggers&lt;br&gt;&lt;br&gt;
webhooks&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;tools&lt;br&gt;&lt;br&gt;
browser, firecrawl, exa&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;providers&lt;br&gt;&lt;br&gt;
openrouter or google for resilience&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;integrations&lt;br&gt;&lt;br&gt;
lightweight API wrappers, not monolithic plugins&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Why this helps
&lt;/h4&gt;

&lt;p&gt;Webhooks act as controlled entry points into the system, turning external events into structured execution.&lt;/p&gt;

&lt;p&gt;Search and scraping tools provide flexibility when APIs are unavailable or inconsistent. Exa and firecrawl handle different retrieval patterns and are worth using together.&lt;/p&gt;

&lt;p&gt;Provider routing reduces dependency on a single model, improving resilience under failure conditions. Integrations are best handled through lightweight API wrappers rather than single all-in-one packages, which keeps failure surfaces small and debugging straightforward.&lt;/p&gt;

&lt;p&gt;The system stops being reactive chat and becomes a component in a larger automation pipeline.&lt;/p&gt;




&lt;h3&gt;
  
  
  Typical OpenClaw Skill Set
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Skill&lt;/th&gt;
&lt;th&gt;Link&lt;/th&gt;
&lt;th&gt;Why it helps&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;taskflow&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/openclaw/openclaw/blob/main/skills/taskflow/SKILL.md" rel="noopener noreferrer"&gt;bundled official skill&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Durable multi-step execution with one owner context across detached tasks. The right abstraction when work spans sessions.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;taskflow-inbox-triage&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/openclaw/openclaw/blob/main/skills/taskflow-inbox-triage/SKILL.md" rel="noopener noreferrer"&gt;bundled official skill&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Concrete pattern for routing inbound work by intent and urgency. Good fit for event-driven pipelines.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tmux&lt;/td&gt;
&lt;td&gt;&lt;a href="https://clawhub.ai/steipete/tmux" rel="noopener noreferrer"&gt;clawhub.ai/steipete/tmux&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Necessary when detached tasks become long-running or require interactive shell sessions.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;session-logs&lt;/td&gt;
&lt;td&gt;&lt;a href="https://clawhub.ai/guogang1024/session-logs" rel="noopener noreferrer"&gt;clawhub.ai/guogang1024/session-logs&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Postmortems are easier when logs are first-class rather than an afterthought.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;blogwatcher&lt;/td&gt;
&lt;td&gt;&lt;a href="https://clawhub.ai/steipete/blogwatcher" rel="noopener noreferrer"&gt;clawhub.ai/steipete/blogwatcher&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Practical for monitoring release feeds, vendor blogs, and changelogs without loading a full scraping stack.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;github&lt;/td&gt;
&lt;td&gt;&lt;a href="https://clawhub.ai/steipete/github" rel="noopener noreferrer"&gt;clawhub.ai/steipete/github&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Incident and release work is often GitHub work. Keeps CI and issue workflows close to the operator.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h4&gt;
  
  
  Why this helps
&lt;/h4&gt;

&lt;p&gt;Automation without structure breaks quickly.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;taskflow introduces multi-step execution ownership across detached sessions&lt;/li&gt;
&lt;li&gt;inbox triage provides a repeatable pattern for routing work by intent and urgency&lt;/li&gt;
&lt;li&gt;tmux enables persistent execution contexts for long-running tasks&lt;/li&gt;
&lt;li&gt;session-logs support debugging, auditability, and postmortems&lt;/li&gt;
&lt;li&gt;blogwatcher handles passive monitoring without a full scraping stack&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Skills provide structure where plugins only provide access.&lt;/p&gt;




&lt;h3&gt;
  
  
  How plugins and skills together serve the automation user
&lt;/h3&gt;

&lt;p&gt;The plugin layer connects OpenClaw to the external world: webhooks bring in events, tools provide flexible data access, provider routing adds resilience.&lt;/p&gt;

&lt;p&gt;The skill layer gives that access structure: taskflow ensures multi-step work maintains ownership and context, triage patterns route incoming work predictably, and logs make failures diagnosable after the fact.&lt;/p&gt;

&lt;p&gt;An ops setup with webhooks but no taskflow skill has triggers but no consistent execution model. A taskflow-based system without provider routing has structure but a single point of failure.&lt;/p&gt;

&lt;p&gt;Together, they make OpenClaw a reliable component in a larger automation pipeline rather than a reactive chat interface.&lt;/p&gt;

&lt;h3&gt;
  
  
  OpenClaw Skill and Plugin Install for Automation and Ops
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Plugins — capabilities layer&lt;/span&gt;
openclaw plugins &lt;span class="nb"&gt;install &lt;/span&gt;webhooks    &lt;span class="c"&gt;# external event triggers over authenticated HTTP routes&lt;/span&gt;
openclaw plugins &lt;span class="nb"&gt;install &lt;/span&gt;browser     &lt;span class="c"&gt;# managed browser profile for dynamic page interaction&lt;/span&gt;
openclaw plugins &lt;span class="nb"&gt;install &lt;/span&gt;firecrawl   &lt;span class="c"&gt;# structured extraction from static and JS-heavy content&lt;/span&gt;
openclaw plugins &lt;span class="nb"&gt;install &lt;/span&gt;exa         &lt;span class="c"&gt;# hybrid search and extraction in one provider&lt;/span&gt;

&lt;span class="c"&gt;# Skills — behavior layer&lt;/span&gt;
&lt;span class="c"&gt;# taskflow and taskflow-inbox-triage are bundled — enable via agent config:&lt;/span&gt;
&lt;span class="c"&gt;# agents.defaults.skills: ["taskflow", "taskflow-inbox-triage"]&lt;/span&gt;

openclaw skills &lt;span class="nb"&gt;install &lt;/span&gt;tmux         &lt;span class="c"&gt;# persistent shell sessions for long-running detached tasks&lt;/span&gt;
openclaw skills &lt;span class="nb"&gt;install &lt;/span&gt;session-logs &lt;span class="c"&gt;# postmortem and audit trail for agent actions&lt;/span&gt;
openclaw skills &lt;span class="nb"&gt;install &lt;/span&gt;blogwatcher  &lt;span class="c"&gt;# monitor release feeds and vendor changelogs without a full scraper&lt;/span&gt;
openclaw skills &lt;span class="nb"&gt;install &lt;/span&gt;github       &lt;span class="c"&gt;# CI, incident, and release workflows from the agent surface&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  3. The Knowledge and Research User
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Profile
&lt;/h3&gt;

&lt;p&gt;This user builds knowledge over time.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;research&lt;/li&gt;
&lt;li&gt;synthesis&lt;/li&gt;
&lt;li&gt;documentation&lt;/li&gt;
&lt;li&gt;analysis&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal is not to execute tasks but to collect, organise, and reuse information across sessions and projects. The system must remember what it has learned and retrieve it accurately.&lt;/p&gt;

&lt;h3&gt;
  
  
  Core needs
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;persistent memory&lt;/li&gt;
&lt;li&gt;high quality retrieval&lt;/li&gt;
&lt;li&gt;traceability&lt;/li&gt;
&lt;li&gt;consistency&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Reliability in this context is less about speed and more about correctness and repeatability. The system should build on prior work rather than repeat the same research each session.&lt;/p&gt;




&lt;h3&gt;
  
  
  Typical OpenClaw Plugin Set
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;memory&lt;br&gt;&lt;br&gt;
memory lancedb, memory wiki&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;search&lt;br&gt;&lt;br&gt;
tavily, exa, firecrawl&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;providers&lt;br&gt;&lt;br&gt;
anthropic or google for large context windows&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Why this helps
&lt;/h4&gt;

&lt;p&gt;Memory plugins turn transient interactions into persistent knowledge. Lancedb provides vector-based retrieval, while wiki-style memory adds structure and traceability so users can verify where information came from.&lt;/p&gt;

&lt;p&gt;Search tools improve input quality, which directly impacts output quality. Tavily and exa provide different retrieval characteristics and are worth using together for research coverage.&lt;/p&gt;

&lt;p&gt;Larger context providers like Anthropic or Google are relevant here because synthesis often requires holding more source material at once than a standard context window allows.&lt;/p&gt;

&lt;p&gt;Without strong memory plugins, research becomes repetitive regardless of how well the skills are configured.&lt;/p&gt;




&lt;h3&gt;
  
  
  Typical OpenClaw Skill Set
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Skill&lt;/th&gt;
&lt;th&gt;Link&lt;/th&gt;
&lt;th&gt;Why it helps&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;multi-search-engine&lt;/td&gt;
&lt;td&gt;&lt;a href="https://clawhub.ai/gpyangyoujun/multi-search-engine" rel="noopener noreferrer"&gt;clawhub.ai/gpyangyoujun/multi-search-engine&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Cross-engine query aggregation with useful operators and time filters. 566 stars, 121k downloads.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;agent-browser&lt;/td&gt;
&lt;td&gt;&lt;a href="https://clawhub.ai/matrixy/agent-browser-clawdbot" rel="noopener noreferrer"&gt;clawhub.ai/matrixy/agent-browser-clawdbot&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Controlled interaction with dynamic pages. Better fit for research than random scraping wrappers. 323 stars, 90.2k downloads.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;blogwatcher&lt;/td&gt;
&lt;td&gt;&lt;a href="https://clawhub.ai/steipete/blogwatcher" rel="noopener noreferrer"&gt;clawhub.ai/steipete/blogwatcher&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Keeps a research corpus fresh through RSS and blog feeds instead of repeated manual browsing. 57 stars, 34.9k downloads.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;nano-pdf&lt;/td&gt;
&lt;td&gt;&lt;a href="https://clawhub.ai/steipete/nano-pdf" rel="noopener noreferrer"&gt;clawhub.ai/steipete/nano-pdf&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;PDF edits, redlines, or document cleanup without switching to a separate tool. 220 stars, 91.5k downloads.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;openai-whisper&lt;/td&gt;
&lt;td&gt;&lt;a href="https://clawhub.ai/steipete/openai-whisper" rel="noopener noreferrer"&gt;clawhub.ai/steipete/openai-whisper&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Local speech-to-text for interview recordings, meeting audio, and field notes. 274 stars, 70.1k downloads.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;notion&lt;/td&gt;
&lt;td&gt;&lt;a href="https://clawhub.ai/steipete/notion" rel="noopener noreferrer"&gt;clawhub.ai/steipete/notion&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Structured team knowledge base for pages and databases. Review secret handling before install. 230 stars, 77.4k downloads.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;obsidian&lt;/td&gt;
&lt;td&gt;&lt;a href="https://clawhub.ai/steipete/obsidian" rel="noopener noreferrer"&gt;clawhub.ai/steipete/obsidian&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Local markdown vault automation for personal knowledge management. High value, review install source. 333 stars, 82.5k downloads.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h4&gt;
  
  
  Why this helps
&lt;/h4&gt;

&lt;p&gt;Skills define how research actually happens.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;multi-search-engine improves discovery quality across sources simultaneously&lt;/li&gt;
&lt;li&gt;agent-browser enables controlled interaction with real web content&lt;/li&gt;
&lt;li&gt;blogwatcher maintains fresh information streams automatically&lt;/li&gt;
&lt;li&gt;pdf and whisper handle real-world data formats that arrive outside clean APIs&lt;/li&gt;
&lt;li&gt;notion and obsidian structure outputs into persistent, queryable knowledge systems&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The system evolves from a query engine into a knowledge engine.&lt;/p&gt;




&lt;h3&gt;
  
  
  How plugins and skills together serve the research user
&lt;/h3&gt;

&lt;p&gt;The plugin layer ensures the system remembers and retrieves reliably: lancedb builds a persistent vector store, wiki memory adds provenance, and search plugins expand the input surface.&lt;/p&gt;

&lt;p&gt;The skill layer determines how research actually flows: multi-search drives discovery, agent-browser handles dynamic sources, blogwatcher maintains ongoing monitoring, and note-taking skills capture outputs into usable formats.&lt;/p&gt;

&lt;p&gt;Without the memory plugin layer, even excellent skills produce knowledge that evaporates after each session. Without the skill layer, even a well-configured memory system sits idle because there is no structured process to feed it.&lt;/p&gt;

&lt;p&gt;See the &lt;a href="https://www.glukhov.org/ai-systems/openclaw/plugins/" rel="noopener noreferrer"&gt;OpenClaw plugins guide&lt;/a&gt; for memory plugin selection and configuration details.&lt;/p&gt;

&lt;h3&gt;
  
  
  OpenClaw Skill and Plugin Install for Knowledge and Research
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Plugins — capabilities layer&lt;/span&gt;
openclaw plugins &lt;span class="nb"&gt;install &lt;/span&gt;memory-lancedb   &lt;span class="c"&gt;# persistent vector memory with auto-recall and capture&lt;/span&gt;
openclaw plugins &lt;span class="nb"&gt;install &lt;/span&gt;memory-wiki      &lt;span class="c"&gt;# structured wiki layer with provenance and contradiction tracking&lt;/span&gt;
openclaw plugins &lt;span class="nb"&gt;install &lt;/span&gt;tavily           &lt;span class="c"&gt;# LLM-optimized structured search and extraction&lt;/span&gt;
openclaw plugins &lt;span class="nb"&gt;install &lt;/span&gt;exa              &lt;span class="c"&gt;# hybrid search modes plus extraction in one provider&lt;/span&gt;
openclaw plugins &lt;span class="nb"&gt;install &lt;/span&gt;firecrawl        &lt;span class="c"&gt;# web_search provider and fallback fetch for JS-heavy pages&lt;/span&gt;

&lt;span class="c"&gt;# Skills — behavior layer&lt;/span&gt;
openclaw skills &lt;span class="nb"&gt;install &lt;/span&gt;multi-search-engine    &lt;span class="c"&gt;# 16-engine aggregation with operators and time filters&lt;/span&gt;
openclaw skills &lt;span class="nb"&gt;install &lt;/span&gt;agent-browser-clawdbot &lt;span class="c"&gt;# controlled browser interaction for dynamic pages&lt;/span&gt;
openclaw skills &lt;span class="nb"&gt;install &lt;/span&gt;blogwatcher            &lt;span class="c"&gt;# RSS and blog feed monitoring to keep corpus fresh&lt;/span&gt;
openclaw skills &lt;span class="nb"&gt;install &lt;/span&gt;nano-pdf               &lt;span class="c"&gt;# PDF editing, redlines, and document cleanup&lt;/span&gt;
openclaw skills &lt;span class="nb"&gt;install &lt;/span&gt;openai-whisper         &lt;span class="c"&gt;# local speech-to-text for recordings and meeting audio&lt;/span&gt;
openclaw skills &lt;span class="nb"&gt;install &lt;/span&gt;notion                 &lt;span class="c"&gt;# structured team knowledge base (review secret handling first)&lt;/span&gt;
&lt;span class="c"&gt;# openclaw skills install obsidian             # local markdown vault — review install source before enabling&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  4. The Customer Support and Communication User
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Profile
&lt;/h3&gt;

&lt;p&gt;This user operates across communication channels.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;customer support&lt;/li&gt;
&lt;li&gt;internal communication&lt;/li&gt;
&lt;li&gt;ticket handling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The challenge is not generating answers but maintaining context across conversations and platforms.&lt;/p&gt;

&lt;h3&gt;
  
  
  Core needs
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;context continuity across conversations&lt;/li&gt;
&lt;li&gt;multi-channel integration&lt;/li&gt;
&lt;li&gt;fast response generation&lt;/li&gt;
&lt;li&gt;auditability&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Typical OpenClaw Plugin Set
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;communication channels&lt;br&gt;&lt;br&gt;
msteams, matrix, wecom, discourse&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;memory&lt;br&gt;&lt;br&gt;
memory lancedb&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;tools&lt;br&gt;&lt;br&gt;
browser&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Why this helps
&lt;/h4&gt;

&lt;p&gt;Channel plugins embed OpenClaw into existing workflows instead of requiring users to switch environments. Where communication happens determines which plugins matter most.&lt;/p&gt;

&lt;p&gt;Memory ensures conversations do not reset between sessions, which is essential for support scenarios where context accumulates over time. A support system without persistent memory forces operators to re-establish context on every interaction.&lt;/p&gt;

&lt;p&gt;Browser access allows the system to retrieve up-to-date information without relying on static integrations — useful when product documentation or policies change frequently.&lt;/p&gt;




&lt;h3&gt;
  
  
  Typical OpenClaw Skill Set
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Skill&lt;/th&gt;
&lt;th&gt;Link&lt;/th&gt;
&lt;th&gt;Why it helps&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;himalaya&lt;/td&gt;
&lt;td&gt;&lt;a href="https://clawhub.ai/lamelas/himalaya" rel="noopener noreferrer"&gt;clawhub.ai/lamelas/himalaya&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Terminal email with triage, reply, forward, search, and organization. One of the cleaner communication skills in the ecosystem. 62 stars, 38.3k downloads.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;slack&lt;/td&gt;
&lt;td&gt;&lt;a href="https://clawhub.ai/steipete/slack" rel="noopener noreferrer"&gt;clawhub.ai/steipete/slack&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Useful when support work lives in Slack. Review undeclared token assumptions before install. 117 stars, 39.1k downloads.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;session-logs&lt;/td&gt;
&lt;td&gt;&lt;a href="https://clawhub.ai/guogang1024/session-logs" rel="noopener noreferrer"&gt;clawhub.ai/guogang1024/session-logs&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Critical for reconstructing prior support interactions and agent decisions. 22 stars, 30.9k downloads.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;nano-pdf&lt;/td&gt;
&lt;td&gt;&lt;a href="https://clawhub.ai/steipete/nano-pdf" rel="noopener noreferrer"&gt;clawhub.ai/steipete/nano-pdf&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Essential when customers send forms, guides, or documents needing quick cleanup or annotation.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;openai-whisper&lt;/td&gt;
&lt;td&gt;&lt;a href="https://clawhub.ai/steipete/openai-whisper" rel="noopener noreferrer"&gt;clawhub.ai/steipete/openai-whisper&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Local speech-to-text for voicemail, support calls, or short media handoffs.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;taskflow-inbox-triage&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/openclaw/openclaw/blob/main/skills/taskflow-inbox-triage/SKILL.md" rel="noopener noreferrer"&gt;bundled official skill&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Workflow pattern for immediate reply, delayed follow-up, and later summary queues.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;notion&lt;/td&gt;
&lt;td&gt;&lt;a href="https://clawhub.ai/steipete/notion" rel="noopener noreferrer"&gt;clawhub.ai/steipete/notion&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Triage notes, FAQ capture, and evolving support playbooks. Fix secret handling before use.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h4&gt;
  
  
  Why this helps
&lt;/h4&gt;

&lt;p&gt;Support workflows are repetitive, structured, and high-stakes.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;himalaya and slack enable direct interaction across the channels where support happens&lt;/li&gt;
&lt;li&gt;session-logs provide the audit trail for prior interactions and agent decisions&lt;/li&gt;
&lt;li&gt;inbox triage structures incoming requests into actionable queues&lt;/li&gt;
&lt;li&gt;whisper and pdf handle real customer inputs that arrive in non-text formats&lt;/li&gt;
&lt;li&gt;notion captures evolving support knowledge into reusable playbooks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Skills reduce cognitive load and standardize response patterns.&lt;/p&gt;




&lt;h3&gt;
  
  
  How plugins and skills together serve the support user
&lt;/h3&gt;

&lt;p&gt;The plugin layer connects OpenClaw to the channels where support actually happens: msteams, matrix, or discourse for channel presence, lancedb for context persistence, browser for live information retrieval.&lt;/p&gt;

&lt;p&gt;The skill layer structures how each interaction is handled: himalaya and slack bring communication directly to the agent surface, inbox triage routes work by urgency, session-logs maintain the audit trail, and notion captures institutional knowledge.&lt;/p&gt;

&lt;p&gt;Support operators touch more customer data than most other roles. That makes the combination of narrow skill sets, per-agent allowlists, and strong auditability especially important. The stack should be smaller than a research stack by design.&lt;/p&gt;

&lt;p&gt;See the &lt;a href="https://www.glukhov.org/ai-systems/openclaw/skills/" rel="noopener noreferrer"&gt;OpenClaw skills guide&lt;/a&gt; for security guidance on communication skills and per-agent allowlist configuration.&lt;/p&gt;

&lt;h3&gt;
  
  
  OpenClaw Skill and Plugin Install for Customer Support and Communication
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Plugins — capabilities layer&lt;/span&gt;
&lt;span class="c"&gt;# Choose the channel plugin that matches your platform:&lt;/span&gt;
openclaw plugins &lt;span class="nb"&gt;install &lt;/span&gt;msteams   &lt;span class="c"&gt;# Microsoft Teams: Azure Bot, tenant credentials, group chat policies&lt;/span&gt;
&lt;span class="c"&gt;# openclaw plugins install matrix  # Matrix: DMs, rooms, threads, media, E2EE&lt;/span&gt;
&lt;span class="c"&gt;# openclaw plugins install wecom   # WeCom: direct messages, group chats, Bot and Agent modes&lt;/span&gt;

openclaw plugins &lt;span class="nb"&gt;install &lt;/span&gt;memory-lancedb   &lt;span class="c"&gt;# persistent conversation context across sessions&lt;/span&gt;
openclaw plugins &lt;span class="nb"&gt;install &lt;/span&gt;browser          &lt;span class="c"&gt;# live information retrieval when docs or policies change&lt;/span&gt;

&lt;span class="c"&gt;# Skills — behavior layer&lt;/span&gt;
&lt;span class="c"&gt;# taskflow-inbox-triage is bundled — enable per agent via config:&lt;/span&gt;
&lt;span class="c"&gt;# agents.list[].skills: ["taskflow-inbox-triage", "himalaya", "session-logs"]&lt;/span&gt;

openclaw skills &lt;span class="nb"&gt;install &lt;/span&gt;himalaya       &lt;span class="c"&gt;# terminal email with triage, reply, forward, and search&lt;/span&gt;
openclaw skills &lt;span class="nb"&gt;install &lt;/span&gt;session-logs   &lt;span class="c"&gt;# audit trail for prior interactions and agent decisions&lt;/span&gt;
openclaw skills &lt;span class="nb"&gt;install &lt;/span&gt;nano-pdf       &lt;span class="c"&gt;# handle forms, guides, and documents from customers&lt;/span&gt;
openclaw skills &lt;span class="nb"&gt;install &lt;/span&gt;openai-whisper &lt;span class="c"&gt;# local speech-to-text for voicemail and support calls&lt;/span&gt;
&lt;span class="c"&gt;# openclaw skills install notion       # triage notes and support playbooks (review secret handling first)&lt;/span&gt;
&lt;span class="c"&gt;# openclaw skills install slack        # Slack channel integration (review token assumptions before enabling)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  5. The Growth and Lead Generation User
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Profile
&lt;/h3&gt;

&lt;p&gt;This user builds pipelines.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;lead discovery&lt;/li&gt;
&lt;li&gt;enrichment&lt;/li&gt;
&lt;li&gt;outreach preparation&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Core needs
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;data collection from public sources&lt;/li&gt;
&lt;li&gt;enrichment and signal extraction&lt;/li&gt;
&lt;li&gt;integration with CRM systems&lt;/li&gt;
&lt;li&gt;repeatability across campaigns&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Typical OpenClaw Plugin Set
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;tools&lt;br&gt;&lt;br&gt;
browser, firecrawl&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;workflow&lt;br&gt;&lt;br&gt;
webhooks&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;integrations&lt;br&gt;&lt;br&gt;
CRM APIs or early-stage connector plugins&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;providers&lt;br&gt;&lt;br&gt;
openrouter for cost-efficient routing&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Why this helps
&lt;/h4&gt;

&lt;p&gt;Browser and firecrawl handle different source types and are worth using together — browser for dynamic interactive pages, firecrawl for structured extraction from static content.&lt;/p&gt;

&lt;p&gt;Webhooks push enriched results into downstream systems such as CRMs or analytics pipelines. Provider routing through openrouter keeps costs predictable when running repeated enrichment passes over large datasets.&lt;/p&gt;

&lt;p&gt;Many growth-focused plugins still show maturity gaps in the ecosystem. Treat them as processing layers rather than systems of record, and verify stability before relying on them in production pipelines.&lt;/p&gt;




&lt;h3&gt;
  
  
  Typical OpenClaw Skill Set
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Skill&lt;/th&gt;
&lt;th&gt;Link&lt;/th&gt;
&lt;th&gt;Why it helps&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;xurl&lt;/td&gt;
&lt;td&gt;&lt;a href="https://clawhub.ai/gaurangzalariya/xurl" rel="noopener noreferrer"&gt;clawhub.ai/gaurangzalariya/xurl&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Converts public X content into pain points, messaging angles, and lead themes without a heavy API-driven setup. 7 stars, 10.2k downloads.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;multi-search-engine&lt;/td&gt;
&lt;td&gt;&lt;a href="https://clawhub.ai/gpyangyoujun/multi-search-engine" rel="noopener noreferrer"&gt;clawhub.ai/gpyangyoujun/multi-search-engine&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Broad prospect and market discovery when one engine never tells the full story. 566 stars, 121k downloads.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;agent-browser&lt;/td&gt;
&lt;td&gt;&lt;a href="https://clawhub.ai/matrixy/agent-browser-clawdbot" rel="noopener noreferrer"&gt;clawhub.ai/matrixy/agent-browser-clawdbot&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Controlled interaction with dynamic prospect pages, forms, or dashboards. 323 stars, 90.2k downloads.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;blogwatcher&lt;/td&gt;
&lt;td&gt;&lt;a href="https://clawhub.ai/steipete/blogwatcher" rel="noopener noreferrer"&gt;clawhub.ai/steipete/blogwatcher&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Monitors competitor posts, launch feeds, and niche sites for ongoing market signals. 57 stars, 34.9k downloads.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;notion&lt;/td&gt;
&lt;td&gt;&lt;a href="https://clawhub.ai/steipete/notion" rel="noopener noreferrer"&gt;clawhub.ai/steipete/notion&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Turns captured signals into structured campaign or pipeline notes. Review secret handling before use.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;openai-whisper&lt;/td&gt;
&lt;td&gt;&lt;a href="https://clawhub.ai/steipete/openai-whisper" rel="noopener noreferrer"&gt;clawhub.ai/steipete/openai-whisper&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Handy for call snippets, voice notes, and quick post-meeting debrief capture.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;slack&lt;/td&gt;
&lt;td&gt;&lt;a href="https://clawhub.ai/steipete/slack" rel="noopener noreferrer"&gt;clawhub.ai/steipete/slack&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Useful for sharing SDR notes and campaign updates. Review token scope before enabling.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h4&gt;
  
  
  Why this helps
&lt;/h4&gt;

&lt;p&gt;Growth workflows rely on signal extraction from public sources.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;xurl extracts themes and pain points from social content without a heavy API setup&lt;/li&gt;
&lt;li&gt;multi-search and agent-browser provide broad and deep discovery across sources&lt;/li&gt;
&lt;li&gt;blogwatcher tracks ongoing market signals and competitor activity&lt;/li&gt;
&lt;li&gt;notion structures raw signal into actionable pipeline assets&lt;/li&gt;
&lt;li&gt;whisper captures voice-based research inputs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Skills transform scattered public data into repeatable outreach inputs.&lt;/p&gt;




&lt;h3&gt;
  
  
  How plugins and skills together serve the growth user
&lt;/h3&gt;

&lt;p&gt;The plugin layer provides data infrastructure: browser and firecrawl gather raw web data, webhooks push enriched results downstream, and openrouter manages cost across repeated enrichment runs.&lt;/p&gt;

&lt;p&gt;The skill layer extracts signal and structures it: xurl surfaces social themes, multi-search broadens discovery coverage, blogwatcher maintains continuous monitoring, and notion converts raw captures into organized pipeline assets.&lt;/p&gt;

&lt;p&gt;Growth setups have a natural tendency toward over-engineering. The most stable configurations stay public-facing and avoid installing every scraping wrapper that promises infinite automation. A focused stack with clear data flow is more durable than an ambitious one that requires constant maintenance.&lt;/p&gt;

&lt;h3&gt;
  
  
  OpenClaw Skill and Plugin Install for Growth and Lead Generation
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Plugins — capabilities layer&lt;/span&gt;
openclaw plugins &lt;span class="nb"&gt;install &lt;/span&gt;browser     &lt;span class="c"&gt;# dynamic page interaction for prospect research and forms&lt;/span&gt;
openclaw plugins &lt;span class="nb"&gt;install &lt;/span&gt;firecrawl   &lt;span class="c"&gt;# structured content extraction from static sources&lt;/span&gt;
openclaw plugins &lt;span class="nb"&gt;install &lt;/span&gt;webhooks    &lt;span class="c"&gt;# push enriched results to CRM and analytics downstream&lt;/span&gt;

&lt;span class="c"&gt;# Skills — behavior layer&lt;/span&gt;
openclaw skills &lt;span class="nb"&gt;install &lt;/span&gt;xurl                   &lt;span class="c"&gt;# extract pain points and messaging angles from public X content&lt;/span&gt;
openclaw skills &lt;span class="nb"&gt;install &lt;/span&gt;multi-search-engine    &lt;span class="c"&gt;# multi-engine prospect and market discovery&lt;/span&gt;
openclaw skills &lt;span class="nb"&gt;install &lt;/span&gt;agent-browser-clawdbot &lt;span class="c"&gt;# controlled interaction with dynamic pages and dashboards&lt;/span&gt;
openclaw skills &lt;span class="nb"&gt;install &lt;/span&gt;blogwatcher            &lt;span class="c"&gt;# monitor competitor posts, launch feeds, and niche sites&lt;/span&gt;
openclaw skills &lt;span class="nb"&gt;install &lt;/span&gt;notion                 &lt;span class="c"&gt;# structure captured signals into campaign pipeline notes (review secret handling first)&lt;/span&gt;
openclaw skills &lt;span class="nb"&gt;install &lt;/span&gt;openai-whisper         &lt;span class="c"&gt;# capture call snippets and voice debrief notes locally&lt;/span&gt;
&lt;span class="c"&gt;# openclaw skills install slack                # share SDR notes and updates (review token scope before enabling)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Cross-cutting production patterns
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Separation of responsibilities
&lt;/h3&gt;

&lt;p&gt;Plugins and skills should not overlap.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;plugins provide capabilities
&lt;/li&gt;
&lt;li&gt;skills define behavior
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Mixing them leads to unpredictable systems where failures are difficult to attribute. When something breaks, you should be able to say immediately whether it is a capability problem or a behavior problem.&lt;/p&gt;




&lt;h3&gt;
  
  
  Start from user intent, not feature lists
&lt;/h3&gt;

&lt;p&gt;Configuration should emerge from what a user actually does, not from what looks impressive.&lt;/p&gt;

&lt;p&gt;Two systems with identical plugins can behave completely differently depending on which skills are loaded and for which agent roles. The skill layer is the real interface.&lt;/p&gt;




&lt;h3&gt;
  
  
  Minimalism wins
&lt;/h3&gt;

&lt;p&gt;More plugins do not mean better systems.&lt;/p&gt;

&lt;p&gt;Production setups converge toward:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;fewer components&lt;/li&gt;
&lt;li&gt;clearer ownership&lt;/li&gt;
&lt;li&gt;predictable behavior&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Adding a component should require justifying what breaks if it is removed. The most effective setups are not the most complex ones.&lt;/p&gt;




&lt;h3&gt;
  
  
  Observability is not optional
&lt;/h3&gt;

&lt;p&gt;Without logs and visibility:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;failures are silent&lt;/li&gt;
&lt;li&gt;debugging is slow&lt;/li&gt;
&lt;li&gt;trust erodes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The session-logs skill and observability plugins like opik openclaw are cheap insurance against invisible failures. They belong in every production setup regardless of user type.&lt;/p&gt;




&lt;h3&gt;
  
  
  Per-agent allowlists matter
&lt;/h3&gt;

&lt;p&gt;OpenClaw's &lt;code&gt;agents.list[].skills&lt;/code&gt; configuration replaces inherited defaults entirely for a given agent role.&lt;/p&gt;

&lt;p&gt;That is the right tool for high-consequence roles like support or finance operators where a narrow, explicit skill set is safer than a broad inherited one.&lt;/p&gt;




&lt;h3&gt;
  
  
  Third-party components need review
&lt;/h3&gt;

&lt;p&gt;Skills from ClawHub should be inspected before install.&lt;/p&gt;

&lt;p&gt;Run &lt;code&gt;clawhub inspect &amp;lt;slug&amp;gt;&lt;/code&gt; to check scan results, declared binaries, and credential use before enabling any community skill in production. Instruction-only skills are safer than code-bearing ones. Bundled official skills are the safest starting point.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://www.glukhov.org/ai-systems/openclaw/skills/" rel="noopener noreferrer"&gt;OpenClaw skills guide&lt;/a&gt; covers the full review workflow and security checklist.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;OpenClaw production systems are not built by installing everything available.&lt;/p&gt;

&lt;p&gt;They are shaped by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;user intent&lt;/li&gt;
&lt;li&gt;workflow structure&lt;/li&gt;
&lt;li&gt;clear separation between capability and behavior&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Plugins make the system powerful.&lt;br&gt;&lt;br&gt;
Skills make it usable.&lt;/p&gt;

&lt;p&gt;The most effective setups are the ones where every component has a clear reason to exist, and every user type has both the capabilities and the structured behaviors needed to do their actual work.&lt;/p&gt;

&lt;p&gt;For next steps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.glukhov.org/ai-systems/openclaw/plugins/" rel="noopener noreferrer"&gt;OpenClaw plugins guide&lt;/a&gt; — plugin lifecycle, ecosystem picks, and safety rails&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.glukhov.org/ai-systems/openclaw/skills/" rel="noopener noreferrer"&gt;OpenClaw skills guide&lt;/a&gt; — ClawHub discovery, per-role stacks, and security tradeoffs&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.glukhov.org/ai-systems/openclaw/quickstart/" rel="noopener noreferrer"&gt;OpenClaw quickstart&lt;/a&gt; — installation with Docker&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>openclaw</category>
      <category>architecture</category>
      <category>selfhosting</category>
      <category>llm</category>
    </item>
    <item>
      <title>Hermes AI Assistant Skills for Real Production Setups</title>
      <dc:creator>Rost</dc:creator>
      <pubDate>Mon, 20 Apr 2026 09:50:09 +0000</pubDate>
      <link>https://dev.to/rosgluk/hermes-ai-assistant-skills-for-real-production-setups-f5f</link>
      <guid>https://dev.to/rosgluk/hermes-ai-assistant-skills-for-real-production-setups-f5f</guid>
      <description>&lt;p&gt;Hermes AI assistant, officially documented as Hermes Agent, is not positioned as a simple chat wrapper.&lt;/p&gt;

&lt;p&gt;For installation, provider setup, tool sandboxing, and gateway configuration, see the &lt;a href="https://www.glukhov.org/ai-systems/hermes/" rel="noopener noreferrer"&gt;Hermes AI Assistant guide&lt;/a&gt;. This article focuses on the skills and profile architecture that determines how Hermes behaves once it is running.&lt;/p&gt;

&lt;p&gt;The official docs and repository describe a self-improving agent with a built-in learning loop that creates skills from experience, improves them during use, persists knowledge across sessions, and runs on anything from a low-cost VPS to cloud sandboxes.&lt;/p&gt;

&lt;p&gt;In April, 2026, the public GitHub repository shows about 94.6k stars, 13.2k forks, and a latest release tagged v0.10.0 on April 16, 2026. That is enough activity to call the project fast-moving, well-adopted, and still operationally young at the same time.&lt;/p&gt;

&lt;p&gt;That dual nature matters for production design. Hermes is mature enough to support real work, but dynamic enough that a messy setup will age badly. The article below treats configuration and skills as an operational architecture question, not as a feature checklist.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Hermes needs a profile-first architecture
&lt;/h2&gt;

&lt;p&gt;Hermes skills are on-demand knowledge documents. They use progressive disclosure so the agent can see a compact skill index first and only load full skill content when needed, which keeps token use under control even when many skills are installed. Every installed skill becomes a slash command in the CLI and in messaging surfaces, and the docs explicitly position skills as the preferred extension mechanism when a capability can be expressed with instructions, shell commands, and existing tools rather than custom agent code.&lt;/p&gt;

&lt;p&gt;The production complication is that Hermes treats skills as living state, not frozen packages. Bundled skills, hub-installed skills, and agent-created skills all live under &lt;code&gt;~/.hermes/skills/&lt;/code&gt;, and the docs state that the agent can modify or delete skills. The same system exposes create, patch, edit, delete, and supporting-file actions for skill management. That is powerful, but it also means one oversized "do everything" agent tends to become a procedural junk drawer.&lt;/p&gt;

&lt;p&gt;Profiles are the answer. Hermes profiles are fully isolated environments, each with its own &lt;code&gt;config.yaml&lt;/code&gt;, &lt;code&gt;.env&lt;/code&gt;, &lt;code&gt;SOUL.md&lt;/code&gt;, memories, sessions, skills, cron jobs, and state database. The CLI also turns a profile into its own command alias, so a profile called &lt;code&gt;coder&lt;/code&gt; becomes &lt;code&gt;coder chat&lt;/code&gt;, &lt;code&gt;coder setup&lt;/code&gt;, &lt;code&gt;coder gateway start&lt;/code&gt;, and so on. In practice, that makes profiles the real unit of production ownership, not the individual skill.&lt;/p&gt;

&lt;h2&gt;
  
  
  The production baseline
&lt;/h2&gt;

&lt;p&gt;The baseline shape is surprisingly clean. Hermes stores non-secret behavior in &lt;code&gt;~/.hermes/config.yaml&lt;/code&gt;, secrets in &lt;code&gt;~/.hermes/.env&lt;/code&gt;, identity in &lt;code&gt;SOUL.md&lt;/code&gt;, persistent facts in &lt;code&gt;memories/&lt;/code&gt;, procedural knowledge in &lt;code&gt;skills/&lt;/code&gt;, scheduled jobs in &lt;code&gt;cron/&lt;/code&gt;, sessions in &lt;code&gt;sessions/&lt;/code&gt;, and logs in &lt;code&gt;logs/&lt;/code&gt;. The &lt;code&gt;hermes config set&lt;/code&gt; command routes API keys into &lt;code&gt;.env&lt;/code&gt; and everything else into &lt;code&gt;config.yaml&lt;/code&gt;, and the documented precedence order is CLI flags first, then &lt;code&gt;config.yaml&lt;/code&gt;, then &lt;code&gt;.env&lt;/code&gt;, then built-in defaults. That is also the cleanest answer to the production FAQ about how secrets and config should be split.&lt;/p&gt;

&lt;p&gt;A practical multi-profile layout usually ends up looking something like this, with one profile per responsibility rather than one profile per human:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;~/.hermes/profiles/
  eng/
  research/
  ops/
  execops/
  ml/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That pattern matches how Hermes profiles are documented: each profile is its own isolated environment, and profiles can be cloned from a base configuration when common defaults are useful. The docs also note that profiles do not share memory or sessions, and that updated skills can be synced across profiles when the main installation is updated.&lt;/p&gt;

&lt;p&gt;The next production boundary is execution. Hermes supports six terminal backends - local, Docker, SSH, Modal, Daytona, and Singularity - and the security docs describe a defense-in-depth model that includes dangerous command approval, container isolation, MCP credential filtering, context file scanning, cross-session isolation, and input sanitization. In other words, the "profile first" decision answers who owns state, and the backend decision answers where risky work is allowed to happen.&lt;/p&gt;

&lt;p&gt;Automation sits on top of that baseline. Hermes cron jobs can attach zero, one, or multiple skills, and they run in fresh agent sessions rather than inheriting the current chat. The messaging gateway is also the background process that manages sessions, runs cron, and routes results back to platforms like Telegram, Discord, Slack, WhatsApp, Email, Matrix, and others. The official MCP guide adds one more production rule that is easy to overlook: the best pattern is not to connect everything, but to expose the smallest useful surface.&lt;/p&gt;

&lt;h2&gt;
  
  
  The software engineering profile
&lt;/h2&gt;

&lt;p&gt;The most obvious Hermes persona is the software engineer who wants the agent to behave less like a chat window and more like a repeatable repo operator. This profile usually cares about repository auth, issue triage, PR creation, code review, debugging, and plan-backed execution. In the Hermes catalogs, the core built-in skill pack is unusually coherent for that job: &lt;code&gt;github-auth&lt;/code&gt;, &lt;code&gt;github-issues&lt;/code&gt;, &lt;code&gt;github-pr-workflow&lt;/code&gt;, &lt;code&gt;github-code-review&lt;/code&gt;, &lt;code&gt;code-review&lt;/code&gt;, &lt;code&gt;plan&lt;/code&gt;, &lt;code&gt;writing-plans&lt;/code&gt;, &lt;code&gt;systematic-debugging&lt;/code&gt;, and &lt;code&gt;test-driven-development&lt;/code&gt;. If delegation matters, Hermes also ships built-in autonomous agent skills such as &lt;code&gt;codex&lt;/code&gt;, &lt;code&gt;claude-code&lt;/code&gt;, &lt;code&gt;opencode&lt;/code&gt;, and &lt;code&gt;hermes-agent-spawning&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;What makes that pack useful is not any single skill. It is the way the skills encode development procedure. &lt;code&gt;github-pr-workflow&lt;/code&gt; covers the full PR lifecycle, &lt;code&gt;github-issues&lt;/code&gt; formalizes issue operations, &lt;code&gt;github-code-review&lt;/code&gt; and &lt;code&gt;code-review&lt;/code&gt; make review a distinct step instead of an afterthought, and &lt;code&gt;systematic-debugging&lt;/code&gt; keeps the agent from jumping straight to premature fixes. That also answers the practical question of which AI assistant skills matter most for coding workflows. The highest-value skills are usually the ones that lock in repo hygiene and review discipline, not the ones that promise more raw code generation.&lt;/p&gt;

&lt;p&gt;Hermes delegation strengthens this profile further. The platform can spawn isolated child agents with their own conversation, terminal session, and toolset, and only the final summary is returned to the parent. For codebases, that is a cleaner fit than stuffing every intermediate diff, stack trace, and review note into one conversation. In production terms, the engineering profile benefits from narrow skill sets, a sandboxed backend such as Docker or SSH, and generous use of delegation when context noise starts to dominate.&lt;/p&gt;

&lt;h2&gt;
  
  
  The research and knowledge profile
&lt;/h2&gt;

&lt;p&gt;The research profile is where Hermes starts to feel distinct from ordinary assistants. The built-in catalogs already include &lt;code&gt;arxiv&lt;/code&gt;, &lt;code&gt;duckduckgo-search&lt;/code&gt;, &lt;code&gt;blogwatcher&lt;/code&gt;, &lt;code&gt;llm-wiki&lt;/code&gt;, &lt;code&gt;ocr-and-documents&lt;/code&gt;, &lt;code&gt;obsidian&lt;/code&gt;, &lt;code&gt;domain-intel&lt;/code&gt;, and &lt;code&gt;ml-paper-writing&lt;/code&gt;, while the official optional catalog adds &lt;code&gt;qmd&lt;/code&gt;, &lt;code&gt;parallel-cli&lt;/code&gt;, &lt;code&gt;scrapling&lt;/code&gt;, and a broader research tier for specialized domains. That stack covers paper search, source monitoring, OCR, local note systems, domain reconnaissance, writing, and hybrid retrieval without forcing everything into a single RAG pattern.&lt;/p&gt;

&lt;p&gt;This profile is also the clearest place to answer the memory-versus-skills question. Hermes documentation defines memory as facts about users, projects, and preferences, while skills store procedures for how to do things. Research work needs both. Memory holds what the assistant has already learned about the domain and the reader's preferences; skills encode repeatable procedures such as "scan arXiv, summarize new papers, and write notes into Obsidian." That distinction matters because production research systems fail when everything is treated as memory or everything is treated as workflow. Hermes gives those concerns separate homes.&lt;/p&gt;

&lt;p&gt;The research profile also benefits disproportionately from cron. Hermes cron jobs can explicitly load skills before execution, and the automation guides stress that scheduled prompts must be fully self-contained because they run in fresh sessions. A recurring pipeline that combines &lt;code&gt;blogwatcher&lt;/code&gt;, &lt;code&gt;arxiv&lt;/code&gt;, &lt;code&gt;obsidian&lt;/code&gt;, or &lt;code&gt;llm-wiki&lt;/code&gt; is therefore more reliable than a vague "check what changed today" job. In other words, research profiles work best when source discovery, note writing, and long-term storage are each represented by a named skill rather than hidden inside one long natural-language prompt.&lt;/p&gt;

&lt;h2&gt;
  
  
  The automation and operations profile
&lt;/h2&gt;

&lt;p&gt;The ops profile is less glamorous and often more valuable. This is the user who wants Hermes to react to events, inspect systems, run scripted checks, route output to a channel, and do all of that without turning the host into a liability. Hermes has the right building blocks for that style of work: built-in &lt;code&gt;webhook-subscriptions&lt;/code&gt; for event-driven activation, built-in &lt;code&gt;native-mcp&lt;/code&gt; and &lt;code&gt;mcporter&lt;/code&gt; for MCP-based tools, and official optional skills such as &lt;code&gt;docker-management&lt;/code&gt;, &lt;code&gt;fastmcp&lt;/code&gt;, &lt;code&gt;cli&lt;/code&gt;, and &lt;code&gt;1password&lt;/code&gt; when the workflow expands into containers, custom MCP servers, or secret injection.&lt;/p&gt;

&lt;p&gt;The reason this pack works is that each skill owns one boundary. &lt;code&gt;webhook-subscriptions&lt;/code&gt; handles ingress from external systems. &lt;code&gt;docker-management&lt;/code&gt; turns container chores into a named procedure instead of a free-form shell game. &lt;code&gt;fastmcp&lt;/code&gt; is useful when Hermes needs to become the orchestrator around new MCP tools, and &lt;code&gt;1password&lt;/code&gt; keeps secret handling explicit rather than smuggled into shell history or markdown files. The official MCP guidance reinforces the same production instinct: connect the right thing with the smallest useful surface.&lt;/p&gt;

&lt;p&gt;This profile is also the cleanest place to answer how scheduled AI workflows stay reliable. Hermes cron documentation says jobs run in fresh sessions, can attach one or more skills, and should use self-contained prompts. The cron troubleshooting guide adds that automatic firing depends on the gateway ticker rather than an ordinary CLI chat session. So the reliable pattern is straightforward even if the implementation is not: explicit skills, explicit delivery target, self-contained prompt, isolated backend, and a gateway that is actually running.&lt;/p&gt;

&lt;h2&gt;
  
  
  The executive operations profile
&lt;/h2&gt;

&lt;p&gt;There is a quieter but very real Hermes persona that looks like a chief of staff, operations lead, or heavily overloaded founder. The relevant skills are less flashy and more office-shaped: &lt;code&gt;google-workspace&lt;/code&gt;, &lt;code&gt;notion&lt;/code&gt;, &lt;code&gt;linear&lt;/code&gt;, &lt;code&gt;nano-pdf&lt;/code&gt;, &lt;code&gt;powerpoint&lt;/code&gt;, and the built-in &lt;code&gt;himalaya&lt;/code&gt; email skill, plus official optional skills such as &lt;code&gt;agentmail&lt;/code&gt;, &lt;code&gt;telephony&lt;/code&gt;, and &lt;code&gt;one-three-one-rule&lt;/code&gt;. That mix gives Hermes access to inbox, calendar, docs, tasks, decks, PDF cleanup, a structured communication framework, and even phone and SMS workflows where that actually matters.&lt;/p&gt;

&lt;p&gt;The flow here is more important than the catalog. &lt;code&gt;google-workspace&lt;/code&gt; anchors day-to-day execution. &lt;code&gt;Notion&lt;/code&gt; and &lt;code&gt;Linear&lt;/code&gt; prevent the assistant from becoming the task system of record. &lt;code&gt;one-three-one-rule&lt;/code&gt; is surprisingly useful because decision support is often the hardest thing to standardize, and that skill gives Hermes a named procedure for proposals rather than generic "summarize this" behavior. &lt;code&gt;nano-pdf&lt;/code&gt; and &lt;code&gt;powerpoint&lt;/code&gt; are the kind of operational multipliers that look small until a team starts touching decks and PDFs every day.&lt;/p&gt;

&lt;p&gt;Hermes messaging and voice features make this profile more practical than it first appears. The gateway can expose the agent through Slack, Telegram, Discord, WhatsApp, Email, Matrix, and several other channels, and the voice stack supports microphone input, spoken replies in messaging, and live Discord voice conversations. The docs also note that one Hermes instance can serve multiple users through allowlists and DM pairing, while bot tokens remain exclusive to a single profile. That is why a communication-heavy deployment usually benefits from at least one dedicated profile instead of sharing the same bot identity with engineering or ops.&lt;/p&gt;

&lt;h2&gt;
  
  
  The ML and data platform profile
&lt;/h2&gt;

&lt;p&gt;Hermes is built by a research lab, and that lineage shows. The catalogs include &lt;code&gt;jupyter-live-kernel&lt;/code&gt; for stateful notebook-style work, &lt;code&gt;huggingface-hub&lt;/code&gt; for model and dataset operations, &lt;code&gt;evaluating-llms-harness&lt;/code&gt; and &lt;code&gt;weights-and-biases&lt;/code&gt; for evaluation and experiment tracking, &lt;code&gt;qdrant-vector-search&lt;/code&gt; for production RAG storage, and a large built-in and optional MLOps tier with skills such as &lt;code&gt;axolotl&lt;/code&gt;, &lt;code&gt;fine-tuning-with-trl&lt;/code&gt;, &lt;code&gt;modal-serverless-gpu&lt;/code&gt;, &lt;code&gt;lambda-labs-gpu-cloud&lt;/code&gt;, &lt;code&gt;flash-attention&lt;/code&gt;, &lt;code&gt;tensorrt-llm&lt;/code&gt;, &lt;code&gt;pinecone&lt;/code&gt;, &lt;code&gt;qdrant&lt;/code&gt;, and &lt;code&gt;nemo-curator&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;What is notable here is not just breadth. It is that the skills span the whole stack from notebook iteration to data curation, evaluation, vector search, fine-tuning, and inference optimization. For an ML platform user, Hermes stops feeling like an assistant and starts feeling like a control plane that can carry procedures across the lifecycle. &lt;code&gt;jupyter-live-kernel&lt;/code&gt; handles iterative exploration, &lt;code&gt;evaluating-llms-harness&lt;/code&gt; and &lt;code&gt;weights-and-biases&lt;/code&gt; formalize measurement, and the optional compute and optimization skills let Hermes talk coherently about both experimentation and deployment.&lt;/p&gt;

&lt;p&gt;This is also the profile where restraint matters most. Because the optional MLOps catalog is so large, a production Hermes setup for ML work usually benefits from being opinionated about scope. A platform engineering profile that owns evaluation and deployment does not need every training framework installed. A research profile that owns papers and note systems does not need every vector database skill. Hermes can carry huge skill inventories, but production usefulness still comes from narrowing the active surface.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where skills become liabilities
&lt;/h2&gt;

&lt;p&gt;The strongest part of the Hermes skills system is also the place where production setups go wrong. Hermes can browse and install skills from its built-in catalog, the official optional catalog, Vercel's &lt;code&gt;skills.sh&lt;/code&gt;, well-known skill endpoints, direct GitHub repositories, and marketplace-style community sources. The security model distinguishes between &lt;code&gt;builtin&lt;/code&gt;, &lt;code&gt;official&lt;/code&gt;, &lt;code&gt;trusted&lt;/code&gt;, and &lt;code&gt;community&lt;/code&gt; sources, runs security scans for hub-installed skills, and allows &lt;code&gt;--force&lt;/code&gt; only for non-dangerous policy blocks. A dangerous scan verdict stays blocked. Hermes also surfaces upstream metadata such as repository URL, weekly installs, and audit signals during inspection. That is a solid trust model, but it is not a substitute for taste.&lt;/p&gt;

&lt;p&gt;There is also a limit to what a skill should be asked to do. Hermes documentation is explicit that skills are the preferred choice when the job can be expressed as instructions plus shell commands plus existing tools, while plugins are the more honest abstraction for custom tools, hooks, and lifecycle behavior. The plugin guide even shows how a plugin can bundle its own skill. In production, that means skills are best treated as reusable procedures, not as a forced substitute for proper tool or plugin design.&lt;/p&gt;

&lt;p&gt;Community and support look healthy, but they do not erase change velocity. Hermes documentation points users to Discord, GitHub Discussions, Issues, and the Skills Hub, and the public repository shows frequent releases and a large contribution footprint. The operational takeaway is simple enough: updates are part of the system, not an event outside it. A real production setup assumes profiles, skills, and workflow assumptions will evolve, then uses isolation and narrow skill packs so that change stays local when it inevitably arrives.&lt;/p&gt;

&lt;p&gt;Hermes works best when skills are treated as procedural contracts around clearly separated profiles. The moment one profile becomes the engineering agent, the research assistant, the ops worker, the inbox bot, and the ML platform all at once, the system stops compounding and starts leaking responsibilities. The clean production pattern is less about having more skills and more about giving each profile a job description it can actually keep.&lt;/p&gt;

&lt;p&gt;This article is part of the &lt;a href="https://www.glukhov.org/ai-systems/" rel="noopener noreferrer"&gt;AI Systems&lt;/a&gt; cluster, which covers self-hosted assistants, retrieval architecture, local LLM infrastructure, and observability.&lt;/p&gt;

</description>
      <category>selfhosting</category>
      <category>hermes</category>
      <category>aiagents</category>
      <category>llm</category>
    </item>
    <item>
      <title>Backup and Restore Gitea server</title>
      <dc:creator>Rost</dc:creator>
      <pubDate>Mon, 20 Apr 2026 09:47:17 +0000</pubDate>
      <link>https://dev.to/rosgluk/backup-and-restore-gitea-server-3l8e</link>
      <guid>https://dev.to/rosgluk/backup-and-restore-gitea-server-3l8e</guid>
      <description>&lt;p&gt;Need to backup the 1) db, 2) filestorage, 3) some other gitea files. Here we go.&lt;/p&gt;

&lt;p&gt;In the &lt;a href="https://www.glukhov.org/developer-tools/git-and-forges/gitea-test1/" rel="noopener noreferrer"&gt;Testing Gitea&lt;/a&gt; post we installed the gitea server.&lt;/p&gt;

&lt;p&gt;For the complete developer tools collection including Git workflows and Docker management, see &lt;a href="https://www.glukhov.org/developer-tools/" rel="noopener noreferrer"&gt;Developer Tools: The Complete Guide to Modern Development Workflows&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you're setting up Gitea for the first time, check out &lt;a href="https://www.glukhov.org/developer-tools/git-and-forges/gitea-test1/" rel="noopener noreferrer"&gt;Choosing free on-prem git server - Gitea is the winner!&lt;/a&gt; for installation details, and &lt;a href="https://www.glukhov.org/developer-tools/git-and-forges/gitea-ssl/" rel="noopener noreferrer"&gt;Gitea SSL with Apache as reverse proxy&lt;/a&gt; for secure deployment.&lt;/p&gt;

&lt;h2&gt;
  
  
  When
&lt;/h2&gt;

&lt;p&gt;Now just as a precaution of terrible things happenings, need to rehearse the backup and restore procedure.&lt;/p&gt;

&lt;p&gt;Better be safe then sorry.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where
&lt;/h2&gt;

&lt;p&gt;Gitea server data consists of 3 components&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;code&lt;/li&gt;
&lt;li&gt;filestore&lt;/li&gt;
&lt;li&gt;db&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In our test environment all togeather they take a bit more then 700MB:&lt;/p&gt;

&lt;p&gt;As they recommend, need to stop all services and back up them all, kind of in the same transaction.&lt;/p&gt;

&lt;p&gt;And restore, in the same transaction too.&lt;/p&gt;

&lt;h2&gt;
  
  
  How - Backup
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ~/gitea-srv-local

&lt;span class="c"&gt;# backup the db&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-t&lt;/span&gt; gitea-srv-local_db_1 bash &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'pg_dump gitea -U gitea  --file=/var/lib/postgresql/backups/gitea-db-$(date +%Y-%m-%d).sql'&lt;/span&gt;

&lt;span class="c"&gt;# take gitea down&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;docker-compose down

&lt;span class="c"&gt;# check the backups folder&lt;/span&gt;
&lt;span class="nb"&gt;sudo ls &lt;/span&gt;postgres-backups

&lt;span class="c"&gt;# create backup dir&lt;/span&gt;
&lt;span class="nb"&gt;mkdir &lt;/span&gt;gitea-backups

&lt;span class="c"&gt;# backup gitea folder&lt;/span&gt;
&lt;span class="nb"&gt;sudo tar&lt;/span&gt; &lt;span class="nt"&gt;-zcvf&lt;/span&gt; gitea-backups/gitea-gitea-&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%Y-%m-%d&lt;span class="si"&gt;)&lt;/span&gt;.tgz gitea/gitea

&lt;span class="c"&gt;# backup repos folder&lt;/span&gt;
&lt;span class="nb"&gt;sudo tar&lt;/span&gt; &lt;span class="nt"&gt;-zcvf&lt;/span&gt; gitea-backups/gitea-git-&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%Y-%m-%d&lt;span class="si"&gt;)&lt;/span&gt;.tgz gitea/git

&lt;span class="c"&gt;# bring it up&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;docker-compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;last bit - login to some other server and pull backup folder there, or do some other more elaborated files manipulation&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;scp &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="nb"&gt;uname&lt;/span&gt;@gitea-srv-ip-addr:/home/uname/gitea-srv-local/gitea-backups ~/gitea-backups
scp &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="nb"&gt;uname&lt;/span&gt;@gitea-srv-ip-addr:/home/uname/gitea-srv-local/postgres-backups ~/postgres-backups

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How - Restore
&lt;/h2&gt;

&lt;p&gt;Actually, there is a bit more then that, esp with permissions and hooks, but idea is the same.&lt;/p&gt;

&lt;p&gt;But! check the original doco: &lt;a href="https://docs.gitea.com/administration/backup-and-restore" rel="noopener noreferrer"&gt;https://docs.gitea.com/administration/backup-and-restore&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# install it first&lt;/span&gt;
&lt;span class="c"&gt;# then take it down&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;docker-compose down

&lt;span class="c"&gt;# restore files&lt;/span&gt;
&lt;span class="nb"&gt;tar&lt;/span&gt; &lt;span class="nt"&gt;-zxvf&lt;/span&gt; gitea-git-___.tgz gitea/git
&lt;span class="nb"&gt;tar&lt;/span&gt; &lt;span class="nt"&gt;-zxvf&lt;/span&gt; gitea-gitea-___.tgz gitea/gitea

&lt;span class="c"&gt;# bring it up&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;docker-compose up &lt;span class="nt"&gt;-d&lt;/span&gt;

&lt;span class="c"&gt;# here some activity with psql or pg_restore&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-t&lt;/span&gt; gitea-srv-local_db_1 bash &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'psql gitea -U gitea  --file=/var/lib/postgresql/backups/gitea-db-$(date +%Y-%m-%d).sql'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then goto UI and check it&lt;/p&gt;

&lt;p&gt;For quick reference on Git commands, see &lt;a href="https://www.glukhov.org/developer-tools/git-and-forges/git-cheatsheet/" rel="noopener noreferrer"&gt;GIT Cheatsheet: Most useful GIT commands&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Useful links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.gitea.com/administration/backup-and-restore" rel="noopener noreferrer"&gt;https://docs.gitea.com/administration/backup-and-restore&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.glukhov.org/developer-tools/containers/docker-cheatsheet/" rel="noopener noreferrer"&gt;Docker Cheatsheet&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.glukhov.org/developer-tools/git-and-forges/gitflow-steps-and-alternatives/" rel="noopener noreferrer"&gt;Gitflow Explained: Steps, Alternatives, Pros, and Cons&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>linux</category>
      <category>selfhosting</category>
      <category>git</category>
      <category>gitea</category>
    </item>
    <item>
      <title>DBeaver vs Beekeeper - SQL Database Management Tools</title>
      <dc:creator>Rost</dc:creator>
      <pubDate>Mon, 20 Apr 2026 09:41:57 +0000</pubDate>
      <link>https://dev.to/rosgluk/dbeaver-vs-beekeeper-sql-database-management-tools-407l</link>
      <guid>https://dev.to/rosgluk/dbeaver-vs-beekeeper-sql-database-management-tools-407l</guid>
      <description>&lt;p&gt;New Linux Ubuntu 24.04 desktop edition has offered me to install Beekeeper Studio as SQL Editor and DB Manager tool.&lt;br&gt;
I was previously using DBeaver.&lt;br&gt;
OK.&lt;br&gt;
Let's &lt;a href="https://www.glukhov.org/developer-tools/database-tools/dbeaver-vs-beekeeper/" rel="noopener noreferrer"&gt;Compare DBeaver with Beekeeper Studio&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This nice image is generated by the model &lt;a href="https://www.glukhov.org/post/2024/09/flux-text-to-image/" rel="noopener noreferrer"&gt;AI model Flux 1 dev&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;TL;DR means &lt;code&gt;too long, didn't read&lt;/code&gt; for those who don't know...&lt;/p&gt;

&lt;p&gt;Beekeeper studio is looking nice but still:&lt;/p&gt;

&lt;p&gt;My choice of best DB management tool is still the same - &lt;a href="https://www.glukhov.org/developer-tools/database-tools/install-dbeaver-on-linux/" rel="noopener noreferrer"&gt;DBeaver&lt;/a&gt; .&lt;br&gt;
Main advantages of the DBeaver in my eyes are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;DBeaver can do backup and restore the SQL DBs&lt;/li&gt;
&lt;li&gt;DBeaver has better license (Apache) comparing to Beekeper Studio (GGPL3)&lt;/li&gt;
&lt;li&gt;In DBeaver you cen select output forma - grid or text. The text is better for copypasting. Don't call it &lt;code&gt;advanced feature&lt;/code&gt;, Beekeeper, please...&lt;/li&gt;
&lt;li&gt;Free Beekeeper Studio feels like intentionally cut version to push everyone to Pro one.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Detailed comparison of &lt;strong&gt;DBeaver&lt;/strong&gt; and &lt;strong&gt;Beekeeper Studio&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;OK, here’s a detailed comparison of &lt;strong&gt;DBeaver&lt;/strong&gt; and &lt;strong&gt;Beekeeper Studio&lt;/strong&gt;, two popular database management tools:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key Differences&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Beekeeper Studio&lt;/th&gt;
&lt;th&gt;DBeaver&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;User Interface&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Modern, user-friendly, fast, and intuitive&lt;/td&gt;
&lt;td&gt;Traditional, robust, may feel complex&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Database Support&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;MySQL, PostgreSQL, SQLite, SQL Server, more&lt;/td&gt;
&lt;td&gt;Relational &amp;amp; NoSQL (MongoDB, Cassandra, etc)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Query Editor&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Intuitive, syntax highlighting, autocomplete&lt;/td&gt;
&lt;td&gt;Comprehensive, execution plan visualization&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Migration Tools&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Streamlined, easy-to-use migration wizards&lt;/td&gt;
&lt;td&gt;Supports migrations, less streamlined&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Data Visualization&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Basic charting, table previews&lt;/td&gt;
&lt;td&gt;Advanced charts, dashboards, reports&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Collaboration&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Built-in collaboration for simultaneous work&lt;/td&gt;
&lt;td&gt;No native collaboration; supports Git&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Learning Curve&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Minimal, easy to start&lt;/td&gt;
&lt;td&gt;Moderate, more features to learn&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Performance&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Lightweight, fast&lt;/td&gt;
&lt;td&gt;Can be slower due to feature density&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;License&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Open source (GPLv3), free &amp;amp; paid tiers&lt;/td&gt;
&lt;td&gt;Open source, free &amp;amp; paid versions&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Strengths
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Beekeeper Studio&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ease of Use:&lt;/strong&gt; Designed for simplicity and speed, with a modern UI that feels like a code editor (similar to VSCode).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quick Start:&lt;/strong&gt; Minimal learning curve, suitable for users who want to get work done without complex setup.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Collaboration:&lt;/strong&gt; Built-in tools for team-based database work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Privacy:&lt;/strong&gt; No telemetry or tracking in the community edition.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;DBeaver&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Feature Density:&lt;/strong&gt; Extensive features for advanced users, including support for a wide range of database types (relational and NoSQL).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data Visualization:&lt;/strong&gt; Advanced charting and reporting tools.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Version Control:&lt;/strong&gt; Integration with Git for team collaboration via code repositories.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Universal Support:&lt;/strong&gt; Broad compatibility with obscure or legacy databases via JDBC.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Use Cases
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Choose Beekeeper Studio&lt;/strong&gt; if you prioritize a fast, modern, and easy-to-use tool for SQL work, especially if you work with mainstream databases and value collaboration and privacy.
For SQL command references, see &lt;a href="https://www.glukhov.org/developer-tools/database-tools/sql-cheatsheet/" rel="noopener noreferrer"&gt;SQL Cheatsheet&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Choose DBeaver&lt;/strong&gt; if you need support for a wide variety of databases (including NoSQL), advanced data visualization, or integration with version control systems.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;DBeaver&lt;/strong&gt; offers superior support for NoSQL databases—including both Redis and MongoDB—compared to Beekeeper Studio.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;DBeaver:&lt;/strong&gt; Supports a wide range of NoSQL databases such as MongoDB, Cassandra, Redis (via JDBC or plugins), and more. Its advanced database management features, including schema browsing, query building, and data visualization, make it a strong choice for users who need to work with various NoSQL solutions. DBeaver’s extensions and plugins further enhance its compatibility with these databases.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Beekeeper Studio:&lt;/strong&gt; Primarily focused on relational databases (e.g., MySQL, PostgreSQL, SQLite, SQL Server). While it is user-friendly and modern, current versions do not provide native or robust support for NoSQL databases like MongoDB or Redis.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Beekeeper Studio offers a more user-friendly and streamlined experience, while DBeaver provides broader database support and advanced features at the cost of a steeper learning curve. The choice depends on your workflow, database needs, and preference for simplicity versus feature richness.&lt;br&gt;
If your primary need is working with NoSQL databases such as Redis and MongoDB, DBeaver is the better choice.&lt;br&gt;
Beekeeper Studio is more suitable for relational database management.&lt;/p&gt;

&lt;p&gt;For PostgreSQL-specific commands, check out the &lt;a href="https://www.glukhov.org/developer-tools/database-tools/postgresql-cheatsheet/" rel="noopener noreferrer"&gt;PostgreSQL Cheatsheet&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;And I like DBeaver more.&lt;/p&gt;

&lt;h2&gt;
  
  
  Useful links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.glukhov.org/developer-tools/database-tools/install-dbeaver-on-linux/" rel="noopener noreferrer"&gt;Install DBeaver on linux&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.glukhov.org/developer-tools/" rel="noopener noreferrer"&gt;Developer Tools: The Complete Guide to Modern Development Workflows&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dbeaver.io" rel="noopener noreferrer"&gt;https://dbeaver.io&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.beekeeperstudio.io" rel="noopener noreferrer"&gt;https://www.beekeeperstudio.io&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>dev</category>
      <category>sql</category>
    </item>
    <item>
      <title>Kubuntu vs KDE Neon: A Technical Deep Dive</title>
      <dc:creator>Rost</dc:creator>
      <pubDate>Sun, 19 Apr 2026 04:48:01 +0000</pubDate>
      <link>https://dev.to/rosgluk/kubuntu-vs-kde-neon-a-technical-deep-dive-48dm</link>
      <guid>https://dev.to/rosgluk/kubuntu-vs-kde-neon-a-technical-deep-dive-48dm</guid>
      <description>&lt;p&gt;For KDE Plasma fans, two Linux distributions frequently come up in discussion:&lt;br&gt;
&lt;a href="https://www.glukhov.org/developer-tools/comparisons/kubuntu-vs-kde-neon/" rel="noopener noreferrer"&gt;&lt;strong&gt;Kubuntu&lt;/strong&gt; and &lt;strong&gt;KDE Neon&lt;/strong&gt;&lt;/a&gt;.&lt;br&gt;
They may appear similar - both ship with KDE Plasma as the default desktop, both are based on Ubuntu, and both are friendly to newcomers. &lt;/p&gt;

&lt;p&gt;But under the hood, they diverge in philosophy, update cadence, and package management. Let's break them down in technical detail.&lt;/p&gt;

&lt;p&gt;For more developer tools comparisons, see &lt;a href="https://www.glukhov.org/developer-tools/" rel="noopener noreferrer"&gt;Developer Tools: The Complete Guide to Modern Development Workflows&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Base System and Repositories
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://kubuntu.org/" rel="noopener noreferrer"&gt;&lt;strong&gt;Kubuntu&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Built as an official &lt;strong&gt;Ubuntu flavor&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Uses &lt;strong&gt;Ubuntu repositories&lt;/strong&gt; (main, universe, multiverse, restricted) plus the &lt;strong&gt;Kubuntu PPAs&lt;/strong&gt; maintained by the Kubuntu team.&lt;/li&gt;
&lt;li&gt;Plasma and KDE applications are &lt;strong&gt;snapshotted&lt;/strong&gt; per &lt;a href="https://www.glukhov.org/developer-tools/terminals-shell/check-linux-ubuntu-version/" rel="noopener noreferrer"&gt;Ubuntu release cycle&lt;/a&gt;, meaning you only get newer KDE versions when upgrading to the next Kubuntu release (unless you manually add backports).&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;a href="https://neon.kde.org/" rel="noopener noreferrer"&gt;&lt;strong&gt;KDE Neon&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Built on top of &lt;strong&gt;Ubuntu LTS releases only&lt;/strong&gt; (e.g., 22.04 LTS).&lt;/li&gt;
&lt;li&gt;Core system packages (kernel, drivers, base libraries) come from Ubuntu LTS repositories.&lt;/li&gt;
&lt;li&gt;KDE packages (Plasma desktop, Frameworks, and Applications) come directly from the &lt;strong&gt;KDE Neon repositories&lt;/strong&gt;, which are maintained by KDE developers.&lt;/li&gt;
&lt;li&gt;Uses a &lt;strong&gt;hybrid model&lt;/strong&gt;: stable Ubuntu LTS base + rolling-release KDE stack.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h2&gt;
  
  
  Update and Release Cycle
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Kubuntu&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Release cycle mirrors Ubuntu: &lt;strong&gt;every six months&lt;/strong&gt; (April and October).&lt;/li&gt;
&lt;li&gt;LTS releases every two years with &lt;strong&gt;5 years of support&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;KDE updates are delivered at the &lt;strong&gt;point release&lt;/strong&gt; stage. Between upgrades, &lt;a href="https://kde.org/plasma-desktop/" rel="noopener noreferrer"&gt;KDE Plasma&lt;/a&gt; versions stay frozen (unless you use the &lt;strong&gt;Kubuntu Backports PPA&lt;/strong&gt;).&lt;/li&gt;
&lt;li&gt;Example: Kubuntu 22.04 shipped with Plasma 5.24 LTS and won’t get Plasma 5.27 unless the user opts into backports.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;strong&gt;KDE Neon&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The Ubuntu base remains fixed (e.g., still on 22.04).&lt;/li&gt;
&lt;li&gt;KDE software is updated &lt;strong&gt;within days of upstream release&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Users receive Plasma point releases, Frameworks, and Application updates through standard APT upgrades.&lt;/li&gt;
&lt;li&gt;Example: Plasma 5.27 becomes available to Neon users almost immediately after KDE publishes it.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h2&gt;
  
  
  Package Management
&lt;/h2&gt;

&lt;p&gt;Both use &lt;strong&gt;APT/dpkg&lt;/strong&gt; as their package management system, but their package sources differ.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Kubuntu&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;apt&lt;/code&gt; pulls from Ubuntu archives and Kubuntu PPAs.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://en.wikipedia.org/wiki/Snap_(software)" rel="noopener noreferrer"&gt;Snap&lt;/a&gt; integration comes by default, as per Ubuntu policy.&lt;/li&gt;
&lt;li&gt;Flatpak available but not preconfigured.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;strong&gt;KDE Neon&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;apt&lt;/code&gt; pulls core from &lt;a href="https://www.glukhov.org/developer-tools/local-dev-platforms/install-linux-ubuntu-24-04/" rel="noopener noreferrer"&gt;Ubuntu LTS&lt;/a&gt; + KDE Neon’s own repos.&lt;/li&gt;
&lt;li&gt;KDE Neon avoids Snap by default, focusing on DEB packages.&lt;/li&gt;
&lt;li&gt;Flatpak is often recommended for newer non-KDE apps.&lt;/li&gt;
&lt;li&gt;Because KDE software is packaged directly by KDE devs, you often see newer versions compared to Ubuntu/Kubuntu.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h2&gt;
  
  
  Kernel and Driver Updates
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Kubuntu&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Follows Ubuntu kernel and driver updates.&lt;/li&gt;
&lt;li&gt;Hardware Enablement (HWE) kernels available on LTS.&lt;/li&gt;
&lt;li&gt;Kernel updates tied to Ubuntu release cycle.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;strong&gt;KDE Neon&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Since the base is Ubuntu LTS, kernel updates come from &lt;strong&gt;Ubuntu LTS + HWE stack&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Neon doesn’t modify kernel or drivers — focus is purely on KDE software.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h2&gt;
  
  
  Stability and Regression Risks
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Kubuntu&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Stable because Plasma and &lt;a href="https://apps.kde.org/" rel="noopener noreferrer"&gt;KDE apps&lt;/a&gt; are frozen until the next release.&lt;/li&gt;
&lt;li&gt;Fewer regressions because software versions are heavily tested.&lt;/li&gt;
&lt;li&gt;Risks come mainly when upgrading between Ubuntu versions (e.g., 22.04 → 22.10).&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;strong&gt;KDE Neon&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;More prone to &lt;strong&gt;regressions&lt;/strong&gt; since you’re on bleeding-edge KDE builds.&lt;/li&gt;
&lt;li&gt;Users sometimes face issues after major Plasma updates (e.g., panel crashes, &lt;a href="https://invent.kde.org/plasma/kwin" rel="noopener noreferrer"&gt;KWin&lt;/a&gt; bugs).&lt;/li&gt;
&lt;li&gt;However, KDE Neon acts as a &lt;strong&gt;testing ground&lt;/strong&gt;, so bugs are quickly reported and patched by KDE devs.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h2&gt;
  
  
  Target Use Cases
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Kubuntu&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Enterprises, developers, and users who want a &lt;strong&gt;“set it and forget it”&lt;/strong&gt; system.&lt;/li&gt;
&lt;li&gt;Ideal for those who rely on long-term stability (e.g., LTS versions).&lt;/li&gt;
&lt;li&gt;Works well in production and business setups.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;strong&gt;KDE Neon&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Enthusiasts, testers, and developers who want &lt;strong&gt;the latest KDE software&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Great for people contributing to KDE or reporting bugs upstream.&lt;/li&gt;
&lt;li&gt;Not always ideal for mission-critical environments due to its rolling KDE nature.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h2&gt;
  
  
  Resource Usage and Performance
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Plasma itself is efficient, and both distros perform similarly on the same hardware.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kubuntu&lt;/strong&gt;: Slightly more conservative with background services, since it adheres to Ubuntu defaults.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Neon&lt;/strong&gt;: Sometimes lighter initially, but Plasma updates may introduce new services or defaults faster than Kubuntu.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Community and Support
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Kubuntu&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Official Ubuntu flavor → benefits from Ubuntu forums, AskUbuntu, Launchpad bug tracking.&lt;/li&gt;
&lt;li&gt;Kubuntu team maintains additional documentation and a strong IRC/Telegram community.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;strong&gt;KDE Neon&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Supported directly by KDE devs and community.&lt;/li&gt;
&lt;li&gt;Bugs in KDE software can be reported &lt;strong&gt;directly upstream to KDE&lt;/strong&gt;, rather than Ubuntu.&lt;/li&gt;
&lt;li&gt;Smaller support base outside of KDE-specific issues, but relies on Ubuntu docs for general system problems.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h2&gt;
  
  
  TL;DR — Key Differences in Table Form
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Kubuntu&lt;/th&gt;
&lt;th&gt;KDE Neon&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Base&lt;/td&gt;
&lt;td&gt;Ubuntu (regular releases + LTS)&lt;/td&gt;
&lt;td&gt;Ubuntu LTS only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Update cycle&lt;/td&gt;
&lt;td&gt;Fixed, tied to Ubuntu&lt;/td&gt;
&lt;td&gt;Rolling KDE on fixed Ubuntu LTS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;KDE updates&lt;/td&gt;
&lt;td&gt;Frozen per release (backports optional)&lt;/td&gt;
&lt;td&gt;Immediate, within days of upstream&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Package sources&lt;/td&gt;
&lt;td&gt;Ubuntu repos + Kubuntu PPAs&lt;/td&gt;
&lt;td&gt;Ubuntu LTS repos + Neon KDE repos&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Snap support&lt;/td&gt;
&lt;td&gt;Included by default&lt;/td&gt;
&lt;td&gt;Not included by default&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stability&lt;/td&gt;
&lt;td&gt;Very stable&lt;/td&gt;
&lt;td&gt;Stable base, but KDE is bleeding-edge&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Target users&lt;/td&gt;
&lt;td&gt;General desktop &amp;amp; enterprise&lt;/td&gt;
&lt;td&gt;KDE enthusiasts, testers, devs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;While &lt;strong&gt;Kubuntu&lt;/strong&gt; is a rock-solid Ubuntu flavor offering a predictable, stable KDE Plasma experience, &lt;strong&gt;KDE Neon&lt;/strong&gt; acts as a rolling showcase of the KDE ecosystem, with Plasma updates delivered almost instantly.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Choose &lt;strong&gt;Kubuntu&lt;/strong&gt; if you want &lt;strong&gt;stability, long-term support, and predictability&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Choose &lt;strong&gt;KDE Neon&lt;/strong&gt; if you want &lt;strong&gt;the latest KDE tech, rapid updates, and direct integration with KDE development&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both are excellent — the decision comes down to whether you prioritize &lt;strong&gt;stability&lt;/strong&gt; or &lt;strong&gt;innovation&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Useful links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.glukhov.org/developer-tools/terminals-shell/bash-cheat-sheet/" rel="noopener noreferrer"&gt;Bash Cheat Sheet&lt;/a&gt; &lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.glukhov.org/developer-tools/local-dev-platforms/install-linux-ubuntu-24-04/" rel="noopener noreferrer"&gt;How to Install Ubuntu 24.04 &amp;amp; useful tools&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.glukhov.org/developer-tools/terminals-shell/reinstall-linux/" rel="noopener noreferrer"&gt;Reinstall linux Mint&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.glukhov.org/developer-tools/terminals-shell/file-managers-for-linux-ubuntu/" rel="noopener noreferrer"&gt;Context menu in File managers for Ubuntu 24.04 - Nautilus vs Nemo vs Dolphin vs Caja&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.glukhov.org/observability/gpu-monitoring-apps-linux/" rel="noopener noreferrer"&gt;GPU monitoring applications in Linux / Ubuntu&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.glukhov.org/developer-tools/comparisons/programming-languages-frameworks-popularity/" rel="noopener noreferrer"&gt;Programming languages and frameworks popularity&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Little Ubuntu Linux Howtos:
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.glukhov.org/developer-tools/terminals-shell/check-linux-ubuntu-version/" rel="noopener noreferrer"&gt;Check Linux Ubuntu Version&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.glukhov.org/developer-tools/terminals-shell/howto-start-terminal-windows-tiled-linux-mint-ubuntu/" rel="noopener noreferrer"&gt;How to start terminal windows tiled linux mint ubuntu&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.glukhov.org/developer-tools/terminals-shell/how-to-change-static-ip-address-in-ubuntu/" rel="noopener noreferrer"&gt;How to Change a Static IP Address in Ubuntu Server&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>linux</category>
      <category>selfhosting</category>
      <category>devops</category>
    </item>
    <item>
      <title>Gitflow Explained: Steps, Alternatives, Pros, and Cons</title>
      <dc:creator>Rost</dc:creator>
      <pubDate>Sun, 19 Apr 2026 04:35:33 +0000</pubDate>
      <link>https://dev.to/rosgluk/gitflow-explained-steps-alternatives-pros-and-cons-10ae</link>
      <guid>https://dev.to/rosgluk/gitflow-explained-steps-alternatives-pros-and-cons-10ae</guid>
      <description>&lt;p&gt;&lt;a href="https://www.glukhov.org/developer-tools/git-and-forges/gitflow-steps-and-alternatives/" rel="noopener noreferrer"&gt;Gitflow&lt;/a&gt; is widely used in projects requiring &lt;strong&gt;versioned releases&lt;/strong&gt;, &lt;strong&gt;parallel development&lt;/strong&gt;, and &lt;strong&gt;hotfix management&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;This guide is part of &lt;a href="https://www.glukhov.org/developer-tools/" rel="noopener noreferrer"&gt;Developer Tools: The Complete Guide to Modern Development Workflows&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;By separating development, testing, and production environments into distinct branches, Gitflow ensures &lt;strong&gt;predictable deployments&lt;/strong&gt; and &lt;strong&gt;clear traceability&lt;/strong&gt; of changes. Its importance lies in its ability to &lt;strong&gt;scale for large teams&lt;/strong&gt; and &lt;strong&gt;maintain stability&lt;/strong&gt; in complex projects.&lt;/p&gt;

&lt;p&gt;Gitflow is a branching model introduced by Vincent Driessen in 2010, designed to manage complex software development workflows with structured release cycles. &lt;/p&gt;

&lt;h2&gt;
  
  
  2. Definition and Core Concept of Gitflow
&lt;/h2&gt;

&lt;p&gt;Gitflow is a &lt;strong&gt;branching strategy&lt;/strong&gt; that organizes workflows around five primary branches:  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;main&lt;/code&gt;/&lt;code&gt;master&lt;/code&gt;&lt;/strong&gt;: Stores production-ready code (stable releases).
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;develop&lt;/code&gt;&lt;/strong&gt;: Acts as the integration branch for ongoing development.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;feature/xxx&lt;/code&gt;&lt;/strong&gt;: Short-lived branches for developing new features.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;release/xxx&lt;/code&gt;&lt;/strong&gt;: Created from &lt;code&gt;develop&lt;/code&gt; to prepare for production releases.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;hotfix/xxx&lt;/code&gt;&lt;/strong&gt;: Branches from &lt;code&gt;main&lt;/code&gt; to address critical production bugs.
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The core concept is to &lt;strong&gt;isolate work&lt;/strong&gt; (features, releases, hotfixes) into dedicated branches, ensuring that production code remains stable while allowing parallel development and testing.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Step-by-Step Sequence of Actions in Gitflow
&lt;/h2&gt;

&lt;p&gt;The Gitflow workflow follows a structured process:  &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Initialize Gitflow&lt;/strong&gt;:

&lt;ul&gt;
&lt;li&gt;Use &lt;code&gt;git flow init&lt;/code&gt; or standard Git commands to set up &lt;code&gt;main&lt;/code&gt; and &lt;code&gt;develop&lt;/code&gt; branches.
&lt;/li&gt;
&lt;li&gt;Before starting, ensure you've &lt;a href="https://www.glukhov.org/developer-tools/git-and-forges/configure-git-username/" rel="noopener noreferrer"&gt;Configure Git User Name and Email Address&lt;/a&gt;.
&lt;/li&gt;
&lt;li&gt;For a comprehensive list of Git commands, see &lt;a href="https://www.glukhov.org/developer-tools/git-and-forges/git-cheatsheet/" rel="noopener noreferrer"&gt;GIT Cheatsheet: Most useful GIT commands&lt;/a&gt;.
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Start a Feature&lt;/strong&gt;:  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create a feature branch from &lt;code&gt;develop&lt;/code&gt;:
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt; git checkout develop  
 git checkout &lt;span class="nt"&gt;-b&lt;/span&gt; feature/new-feature  
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;(Alternative): &lt;code&gt;git flow feature start new-feature&lt;/code&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Develop the Feature&lt;/strong&gt;:
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;Commit changes to the feature branch.

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Finish the Feature&lt;/strong&gt;:
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Merge into &lt;code&gt;develop&lt;/code&gt; and delete the branch:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt; git checkout develop  
 git merge feature/new-feature  
 git branch &lt;span class="nt"&gt;-d&lt;/span&gt; feature/new-feature  
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;(Alternative): &lt;code&gt;git flow feature finish new-feature&lt;/code&gt;  &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Prepare a Release&lt;/strong&gt;:
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Create a release branch from &lt;code&gt;develop&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt; git checkout develop  
 git checkout &lt;span class="nt"&gt;-b&lt;/span&gt; release/1.2.0  
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;(Alternative): &lt;code&gt;git flow release start 1.2.0&lt;/code&gt;  &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Finalize the Release&lt;/strong&gt;:
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Merge into &lt;code&gt;main&lt;/code&gt; and &lt;code&gt;develop&lt;/code&gt;, tag the release:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt; git checkout main  
 git merge release/1.2.0  
 git tag &lt;span class="nt"&gt;-a&lt;/span&gt; 1.2.0 &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Release version 1.2.0"&lt;/span&gt;  
 git checkout develop  
 git merge release/1.2.0  
 git branch &lt;span class="nt"&gt;-d&lt;/span&gt; release/1.2.0  
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;(Alternative): &lt;code&gt;git flow release finish 1.2.0&lt;/code&gt;  &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Handle Hotfixes&lt;/strong&gt;:
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Create a hotfix branch from &lt;code&gt;main&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt; git checkout main  
 git checkout &lt;span class="nt"&gt;-b&lt;/span&gt; hotfix/critical-bug  
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;&lt;p&gt;(Alternative): &lt;code&gt;git flow hotfix start critical-bug&lt;/code&gt;  &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Merge into &lt;code&gt;main&lt;/code&gt; and &lt;code&gt;develop&lt;/code&gt;, tag the hotfix:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt; git checkout main  
 git merge hotfix/critical-bug  
 git tag &lt;span class="nt"&gt;-a&lt;/span&gt; 1.2.1 &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Hotfix version 1.2.1"&lt;/span&gt;  
 git checkout develop  
 git merge hotfix/critical-bug  
 git branch &lt;span class="nt"&gt;-d&lt;/span&gt; hotfix/critical-bug  
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;&lt;p&gt;(Alternative): &lt;code&gt;git flow hotfix finish critical-bug&lt;/code&gt;  &lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  4. Typical Workflow Stages and Branching Strategy
&lt;/h2&gt;

&lt;p&gt;Gitflowâ€™s branching strategy ensures &lt;strong&gt;separation of concerns&lt;/strong&gt;:  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Feature branches&lt;/strong&gt; allow parallel development without affecting &lt;code&gt;develop&lt;/code&gt;.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Release branches&lt;/strong&gt; provide a &lt;strong&gt;testing environment&lt;/strong&gt; for finalizing releases.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hotfix branches&lt;/strong&gt; enable &lt;strong&gt;urgent bug fixes&lt;/strong&gt; without disrupting ongoing development.
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Key stages include:  &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Feature Development&lt;/strong&gt; â†’ 2. &lt;strong&gt;Integration into &lt;code&gt;develop&lt;/code&gt;&lt;/strong&gt; â†’ 3. &lt;strong&gt;Release Preparation&lt;/strong&gt; â†’ 4. &lt;strong&gt;Stabilization and Deployment&lt;/strong&gt; â†’ 5. &lt;strong&gt;Hotfix Handling&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  5. Common Use Cases and Scenarios for Gitflow
&lt;/h2&gt;

&lt;p&gt;Gitflow is ideal for:  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Large teams&lt;/strong&gt; requiring &lt;strong&gt;structured collaboration&lt;/strong&gt;.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Projects with scheduled releases&lt;/strong&gt; (e.g., enterprise software, regulated industries).
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Complex systems&lt;/strong&gt; requiring &lt;strong&gt;versioned deployments&lt;/strong&gt; (e.g., multi-tenant applications).
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Teams needing isolation&lt;/strong&gt; between development, testing, and production environments.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  6. Overview of Gitflow Alternatives
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;GitHub Flow&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Workflow&lt;/strong&gt;: Single &lt;code&gt;main&lt;/code&gt; branch with short-lived feature branches.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Steps&lt;/strong&gt;:

&lt;ol&gt;
&lt;li&gt;Create a feature branch from &lt;code&gt;main&lt;/code&gt;.
&lt;/li&gt;
&lt;li&gt;Merge via pull request after testing.
&lt;/li&gt;
&lt;li&gt;Deploy directly to production.
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Advantages&lt;/strong&gt;: Simplicity, CI/CD compatibility, rapid deployment.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Disadvantages&lt;/strong&gt;: No structured release management; unsuitable for versioned projects.
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;GitLab Flow&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Workflow&lt;/strong&gt;: Combines GitHub Flow with environment-specific branches (e.g., &lt;code&gt;staging&lt;/code&gt;, &lt;code&gt;production&lt;/code&gt;).
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Advantages&lt;/strong&gt;: Balances simplicity and structure for hybrid workflows.
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Trunk-Based Development&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Workflow&lt;/strong&gt;: All changes are merged directly into &lt;code&gt;main&lt;/code&gt; using &lt;strong&gt;feature flags&lt;/strong&gt;.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Advantages&lt;/strong&gt;: Reduces branching overhead, supports CI/CD.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Disadvantages&lt;/strong&gt;: Requires mature testing pipelines and disciplined teams.
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Branch Per Feature&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Workflow&lt;/strong&gt;: Each feature is developed in its own branch, merged into &lt;code&gt;main&lt;/code&gt; after testing.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Advantages&lt;/strong&gt;: Isolates features, reduces conflicts.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Adoption&lt;/strong&gt;: Used by companies like Spotify and Netflix.
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  7. Weaknesses and Limitations of Gitflow
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Complexity&lt;/strong&gt;:

&lt;ul&gt;
&lt;li&gt;Managing multiple branches increases &lt;strong&gt;merge conflicts&lt;/strong&gt; and &lt;strong&gt;overhead&lt;/strong&gt;.
&lt;/li&gt;
&lt;li&gt;Requires strict &lt;strong&gt;branch hygiene&lt;/strong&gt; and discipline.
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not Ideal for CI/CD&lt;/strong&gt;:

&lt;ul&gt;
&lt;li&gt;The branching model is &lt;strong&gt;rigid&lt;/strong&gt; for continuous delivery environments.
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Risk of Merge Conflicts&lt;/strong&gt;:

&lt;ul&gt;
&lt;li&gt;Long-lived branches (e.g., &lt;code&gt;develop&lt;/code&gt;, &lt;code&gt;release&lt;/code&gt;) can diverge, leading to integration issues.
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Learning Curve&lt;/strong&gt;:

&lt;ul&gt;
&lt;li&gt;New developers may struggle with &lt;strong&gt;branching rules&lt;/strong&gt; and &lt;strong&gt;merge strategies&lt;/strong&gt;.
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Slower Releases&lt;/strong&gt;:

&lt;ul&gt;
&lt;li&gt;Multi-step processes (e.g., release â†’ &lt;code&gt;develop&lt;/code&gt; â†’ &lt;code&gt;main&lt;/code&gt;) can delay deployments.
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  8. Advantages and Benefits of Using Gitflow
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Structured Release Management&lt;/strong&gt;:

&lt;ul&gt;
&lt;li&gt;Clear separation of features, releases, and hotfixes.
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stability&lt;/strong&gt;:

&lt;ul&gt;
&lt;li&gt;Ensures &lt;code&gt;main&lt;/code&gt; remains &lt;strong&gt;production-ready&lt;/strong&gt; at all times.
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Version Control&lt;/strong&gt;:

&lt;ul&gt;
&lt;li&gt;Semantic versioning and tagging improve &lt;strong&gt;traceability&lt;/strong&gt; and &lt;strong&gt;reproducibility&lt;/strong&gt;.
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Collaboration&lt;/strong&gt;:

&lt;ul&gt;
&lt;li&gt;Enables &lt;strong&gt;parallel development&lt;/strong&gt; and &lt;strong&gt;isolated testing&lt;/strong&gt;.
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hotfix Efficiency&lt;/strong&gt;:

&lt;ul&gt;
&lt;li&gt;Critical fixes can be applied to &lt;code&gt;main&lt;/code&gt; without disrupting ongoing development.
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  9. Comparison: Gitflow vs. Alternative Workflows
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Aspect&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Gitflow&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;GitHub Flow&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Trunk-Based Development&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Branching Model&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Multi-branch (feature, develop, release, hotfix, main)&lt;/td&gt;
&lt;td&gt;Minimal (main + feature branches)&lt;/td&gt;
&lt;td&gt;Single &lt;code&gt;main&lt;/code&gt; branch with feature flags&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Release Process&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Structured with release branches&lt;/td&gt;
&lt;td&gt;Direct deployment from main&lt;/td&gt;
&lt;td&gt;Continuous deployment from &lt;code&gt;main&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Complexity&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;High (suitable for large projects)&lt;/td&gt;
&lt;td&gt;Low (ideal for agile, small teams)&lt;/td&gt;
&lt;td&gt;Low (requires mature CI/CD)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Merge Frequency&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Frequent (across multiple branches)&lt;/td&gt;
&lt;td&gt;Minimal (fewer merges)&lt;/td&gt;
&lt;td&gt;Frequent (direct to &lt;code&gt;main&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Testing Requirements&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Rigorous (for release/hotfix branches)&lt;/td&gt;
&lt;td&gt;Automated tests critical for main&lt;/td&gt;
&lt;td&gt;Automated tests for feature flags&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  10. Best Practices for Implementing Gitflow
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Automate Workflows&lt;/strong&gt;: Use CI/CD tools (e.g., Jenkins, GitHub Actions) to reduce manual effort.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enforce Branch Naming Conventions&lt;/strong&gt;: Standardize branch names (e.g., &lt;code&gt;feature/{name}&lt;/code&gt;) for clarity.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Regular Sync Meetings&lt;/strong&gt;: Ensure alignment between teams to address bottlenecks.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automated Dependency Management&lt;/strong&gt;: Use tools like Dependabot to manage outdated dependencies.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Merge Strategy&lt;/strong&gt;: Use &lt;code&gt;--no-ff&lt;/code&gt; merges to preserve feature history.
&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  11. Case Studies or Real-World Examples
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Large Enterprises&lt;/strong&gt;: Companies like &lt;strong&gt;Microsoft&lt;/strong&gt; and &lt;strong&gt;IBM&lt;/strong&gt; use Gitflow for managing complex releases in legacy systems.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open-Source Projects&lt;/strong&gt;: Gitflow is less common in open-source due to its complexity but is used in projects requiring &lt;strong&gt;long-term maintenance&lt;/strong&gt; (e.g., &lt;strong&gt;Kubernetes&lt;/strong&gt;).
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hybrid Workflows&lt;/strong&gt;: Teams like &lt;strong&gt;GitLab&lt;/strong&gt; use &lt;strong&gt;GitLab Flow&lt;/strong&gt; to combine Gitflowâ€™s structure with GitHub Flowâ€™s simplicity.
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  12. Conclusion and Final Thoughts on Gitflowâ€™s Relevance
&lt;/h2&gt;

&lt;p&gt;Gitflow remains a &lt;strong&gt;robust solution&lt;/strong&gt; for &lt;strong&gt;structured release management&lt;/strong&gt; in large, complex projects. Its strengths in &lt;strong&gt;version control&lt;/strong&gt;, &lt;strong&gt;stability&lt;/strong&gt;, and &lt;strong&gt;collaboration&lt;/strong&gt; make it ideal for teams with &lt;strong&gt;scheduled release cycles&lt;/strong&gt; and &lt;strong&gt;regulatory compliance&lt;/strong&gt; requirements. However, its &lt;strong&gt;complexity&lt;/strong&gt; and &lt;strong&gt;overhead&lt;/strong&gt; make it less suitable for &lt;strong&gt;small teams&lt;/strong&gt;, &lt;strong&gt;agile environments&lt;/strong&gt;, or &lt;strong&gt;CI/CD pipelines&lt;/strong&gt;.  &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Alternatives&lt;/strong&gt; like GitHub Flow (for simplicity) and Trunk-Based Development (for CI/CD) offer &lt;strong&gt;trade-offs&lt;/strong&gt; in flexibility and scalability. The choice of workflow depends on &lt;strong&gt;team size&lt;/strong&gt;, &lt;strong&gt;project complexity&lt;/strong&gt;, and &lt;strong&gt;release frequency&lt;/strong&gt;. As DevOps practices evolve, Gitflowâ€™s role may shift toward &lt;strong&gt;hybrid models&lt;/strong&gt; that combine its structure with modern automation tools.  &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Final Recommendation&lt;/strong&gt;:  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Use Gitflow&lt;/strong&gt; for large-scale, versioned projects.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Adopt GitHub Flow or Trunk-Based Development&lt;/strong&gt; for smaller teams or CI/CD environments.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Customize workflows&lt;/strong&gt; based on team needs and project scope.
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Useful links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.glukhov.org/developer-tools/git-and-forges/gitea-test1/" rel="noopener noreferrer"&gt;Gitea&lt;/a&gt; &lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.glukhov.org/ai-devtools/vibe-coding/" rel="noopener noreferrer"&gt;What is Vibe Coding?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.glukhov.org/developer-tools/comparisons/programming-languages-frameworks-popularity/" rel="noopener noreferrer"&gt;Programming languages and frameworks popularity&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.glukhov.org/post/2025/05/python-venv-cheatsheet/" rel="noopener noreferrer"&gt;venv Cheatsheet&lt;/a&gt; &lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.glukhov.org/documentation-tools/pdf/generating-pdf-in-python/" rel="noopener noreferrer"&gt;Generating PDF in Python - Libraries and examples&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.glukhov.org/data-infrastructure/object-storage/minio-vs-aws-s3/" rel="noopener noreferrer"&gt;Minio as Aws S3 alternative. Minio overview and install&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.glukhov.org/developer-tools/editors-ides/vscode-cheatsheet/" rel="noopener noreferrer"&gt;VSCode Cheatsheet&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>dev</category>
      <category>devops</category>
      <category>git</category>
      <category>gitea</category>
    </item>
    <item>
      <title>PostgreSQL Full Text Search vs Elasticsearch Comparison</title>
      <dc:creator>Rost</dc:creator>
      <pubDate>Fri, 17 Apr 2026 12:57:21 +0000</pubDate>
      <link>https://dev.to/rosgluk/postgresql-full-text-search-vs-elasticsearch-comparison-2dcn</link>
      <guid>https://dev.to/rosgluk/postgresql-full-text-search-vs-elasticsearch-comparison-2dcn</guid>
      <description>&lt;p&gt;The real argument is not whether PostgreSQL can search text or whether Elasticsearch can store documents.&lt;br&gt;
Both can.&lt;br&gt;
The interesting question is where search complexity should live.&lt;/p&gt;



&lt;p&gt;PostgreSQL full text search lives inside a transactional relational database with &lt;code&gt;tsvector&lt;/code&gt;, &lt;code&gt;tsquery&lt;/code&gt;, dictionaries, ranking, and GIN indexes. Elasticsearch is a distributed search and analytics engine built on Lucene, with analyzers, BM25 scoring, shard-based scale, aggregations, and near-real-time indexing.&lt;/p&gt;

&lt;p&gt;Those are different operational philosophies before they are different feature lists.&lt;/p&gt;

&lt;p&gt;If you are mapping this choice to storage, pipelines, and operations, &lt;a href="https://www.glukhov.org/data-infrastructure/" rel="noopener noreferrer"&gt;this data infrastructure overview&lt;/a&gt; gives the wider system context.&lt;/p&gt;
&lt;h2&gt;
  
  
  What this comparison is actually about
&lt;/h2&gt;

&lt;p&gt;At a low level, both systems rely on inverted-index ideas, but they package them very differently. PostgreSQL recommends GIN as the preferred text-search index type and describes it as an inverted index over lexemes in &lt;code&gt;tsvector&lt;/code&gt; values. Elasticsearch analyzes &lt;code&gt;text&lt;/code&gt; fields and indexes them for full-text search, then distributes those indexes across shards and nodes for scale. In practice, PostgreSQL feels like search embedded in your application database, while Elasticsearch feels like a dedicated search platform with its own runtime, lifecycle, and scaling model.&lt;/p&gt;

&lt;p&gt;This comparison is mostly about native PostgreSQL full text search plus the very common &lt;code&gt;pg_trgm&lt;/code&gt; helper for fuzzy-ish matching. That scope matters because the broader PostgreSQL ecosystem is getting more search-heavy over time. Extensions such as RUM add richer index behavior for phrase search and ranking-oriented scans, while PGroonga extends PostgreSQL with another full-text indexing path. That does not make native PostgreSQL equal to Elasticsearch, but it does mean the boundary is less static than many old comparisons assume.&lt;/p&gt;

&lt;p&gt;My opinionated framing is simple. Search is usually a feature until it becomes a product surface. PostgreSQL tends to win while search is still a feature. Elasticsearch tends to win when search becomes the thing users judge first. That is less about brand names and more about where relevance logic, indexing policy, and operational pain are allowed to live.&lt;/p&gt;
&lt;h2&gt;
  
  
  How PostgreSQL full text search works
&lt;/h2&gt;

&lt;p&gt;PostgreSQL full text search starts by turning raw text into lexemes. &lt;code&gt;to_tsvector&lt;/code&gt; tokenizes text, normalizes it through the configured dictionaries, drops stop words, and stores surviving lexemes with positions. &lt;code&gt;setweight&lt;/code&gt; lets you label lexemes from different parts of the document, such as title, abstract, and body, so those parts can influence ranking differently. PostgreSQL also supports multiple predefined language configurations and lets you build custom configurations with parsers and dictionaries.&lt;br&gt;
If you want a compact SQL reference while implementing these patterns, &lt;a href="https://www.glukhov.org/developer-tools/database-tools/postgresql-cheatsheet/" rel="noopener noreferrer"&gt;this PostgreSQL cheatsheet&lt;/a&gt; and &lt;a href="https://www.glukhov.org/developer-tools/database-tools/sql-cheatsheet/" rel="noopener noreferrer"&gt;this SQL cheatsheet with the most useful SQL commands&lt;/a&gt; are practical companions.&lt;/p&gt;

&lt;p&gt;A typical production pattern is a stored generated &lt;code&gt;tsvector&lt;/code&gt; column plus a GIN index. PostgreSQL's documentation is blunt that practical text search usually requires an index, and it explicitly shows a stored generated column feeding a GIN index. That pattern avoids recomputing &lt;code&gt;to_tsvector&lt;/code&gt; during verification and keeps the query surface clean.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;alter&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="n"&gt;posts&lt;/span&gt;
  &lt;span class="k"&gt;add&lt;/span&gt; &lt;span class="k"&gt;column&lt;/span&gt; &lt;span class="n"&gt;search_vector&lt;/span&gt; &lt;span class="n"&gt;tsvector&lt;/span&gt;
  &lt;span class="k"&gt;generated&lt;/span&gt; &lt;span class="n"&gt;always&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;setweight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;to_tsvector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'english'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;coalesce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="s1"&gt;'A'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
    &lt;span class="n"&gt;setweight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;to_tsvector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'english'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;coalesce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="s1"&gt;'B'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
    &lt;span class="n"&gt;setweight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;to_tsvector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'english'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;coalesce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="s1"&gt;'D'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;stored&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="k"&gt;index&lt;/span&gt; &lt;span class="n"&gt;posts_search_idx&lt;/span&gt;
  &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;posts&lt;/span&gt; &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="n"&gt;gin&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;search_vector&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;ts_rank_cd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
         &lt;span class="n"&gt;search_vector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="n"&gt;websearch_to_tsquery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'english'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'"query planner" -mysql'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
       &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;rank&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;posts&lt;/span&gt;
&lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;search_vector&lt;/span&gt; &lt;span class="o"&gt;@@&lt;/span&gt; &lt;span class="n"&gt;websearch_to_tsquery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'english'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'"query planner" -mysql'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;order&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="n"&gt;rank&lt;/span&gt; &lt;span class="k"&gt;desc&lt;/span&gt;
&lt;span class="k"&gt;limit&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the query side, PostgreSQL gives you several parsers because user input is messier than engineering blogs admit. &lt;code&gt;to_tsquery&lt;/code&gt; is explicit and powerful. &lt;code&gt;phraseto_tsquery&lt;/code&gt; preserves word order with the &lt;code&gt;&amp;lt;-&amp;gt;&lt;/code&gt; operator. &lt;code&gt;websearch_to_tsquery&lt;/code&gt; accepts search-engine-like input, understands quoted phrases, &lt;code&gt;OR&lt;/code&gt;, and &lt;code&gt;-&lt;/code&gt; negation, and never raises syntax errors on raw user input. PostgreSQL also supports prefix matching by attaching &lt;code&gt;*&lt;/code&gt; to a lexeme in &lt;code&gt;to_tsquery&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Ranking is where native PostgreSQL shows both its strength and its ceiling. &lt;code&gt;ts_rank&lt;/code&gt; and &lt;code&gt;ts_rank_cd&lt;/code&gt; can use frequency, proximity, and structural weights, and the weighting model is surprisingly good for many application search tasks. At the same time, PostgreSQL's own docs note that ranking can be expensive and that the built-in ranking functions do not use global information. That is the quiet but important limit of native PostgreSQL full text search. It can rank, but relevance is not the center of gravity of the engine.&lt;/p&gt;

&lt;h3&gt;
  
  
  When PostgreSQL is enough for full text search
&lt;/h3&gt;

&lt;p&gt;PostgreSQL is enough more often than dedicated search vendors would like. It is particularly compelling when search stays very close to transactional rows, joins, permissions, and fresh writes. PostgreSQL's MVCC model provides transactional consistency and snapshot-based reads, so the same database that accepts the write can answer the search without an Elasticsearch-style refresh window. When a search box is really "find records inside the app I just edited," that property matters more than glossy relevance demos.&lt;/p&gt;

&lt;p&gt;It is also enough when SQL filtering is half the feature. Status filters, tenant isolation, publication states, timestamps, and relational joins often matter just as much as keyword relevance in line-of-business systems. In those cases, PostgreSQL full text search behaves like another indexed predicate in a relational query plan, not like a separate platform that needs to be fed and kept warm. That is a boring architecture, and boring is often the right kind of fast.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Elasticsearch works as a search engine
&lt;/h2&gt;

&lt;p&gt;Elasticsearch presents itself very differently. Its own docs define it as a distributed search and analytics engine, scalable data store, and vector database built on Apache Lucene, optimized for speed and relevance at production scale and operating in near real time. Elasticsearch splits each index into shards, replicates those shards, and distributes them across nodes to increase indexing and query capacity. This is why Elasticsearch is rarely "just an index." It is a cluster architecture.&lt;/p&gt;

&lt;p&gt;Under the hood, analyzers do most of the heavy lifting. An Elasticsearch analyzer is a composition of character filters, tokenizers, and token filters. There are built-in analyzers, language analyzers, and custom analyzers, and synonym handling is a first-class part of analysis. That means search behavior is not only about the query. It is also about how both documents and queries are normalized before scoring even begins.&lt;/p&gt;

&lt;p&gt;For a hands-on API reference while implementing these patterns, &lt;a href="https://www.glukhov.org/data-infrastructure/search/elasticsearch-cheatsheet/" rel="noopener noreferrer"&gt;this Elasticsearch cheatsheet&lt;/a&gt; collects essential commands and operational shortcuts.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;PUT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;posts&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mappings"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"properties"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"text"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"summary"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"text"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"body"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"text"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"tags"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"keyword"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;GET&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;posts/_search&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"query"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"bool"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"must"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"multi_match"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"query"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"query planner"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"fields"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"title^3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"summary^2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"body"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"best_fields"&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"must_not"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"match"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"body"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mysql"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"aggs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"by_tag"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"terms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"field"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tags"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"highlight"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"fields"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"body"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Query DSL is where Elasticsearch starts to feel search-native rather than database-like. &lt;code&gt;bool&lt;/code&gt; combines clauses with &lt;code&gt;must&lt;/code&gt;, &lt;code&gt;should&lt;/code&gt;, &lt;code&gt;filter&lt;/code&gt;, and &lt;code&gt;must_not&lt;/code&gt;. &lt;code&gt;multi_match&lt;/code&gt; can search across many fields with field boosts and different execution modes such as &lt;code&gt;best_fields&lt;/code&gt;, &lt;code&gt;most_fields&lt;/code&gt;, &lt;code&gt;cross_fields&lt;/code&gt;, &lt;code&gt;phrase&lt;/code&gt;, and &lt;code&gt;bool_prefix&lt;/code&gt;. Aggregations, highlights, and filters can all sit alongside the main query in the same request. BM25 is the default similarity model.&lt;/p&gt;

&lt;p&gt;The freshness model is also explicit. Elasticsearch is near real time, not immediately search-consistent. Recent operations become visible to search when a refresh opens a new segment, and by default that refresh happens every second on indices that have been searched recently. Elastic's docs also warn that refreshes are resource-intensive and recommend waiting for periodic refreshes or using &lt;code&gt;refresh=wait_for&lt;/code&gt; when a workflow needs read-after-write search visibility. That is a very different contract from PostgreSQL.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Elasticsearch usually ranks complex search better
&lt;/h3&gt;

&lt;p&gt;This is the deepest technical reason many teams eventually move from PostgreSQL full text search to Elasticsearch. PostgreSQL's built-in ranking functions do not use global information, while Elasticsearch uses BM25 by default and exposes field-specific similarity settings, analyzers, multi-field query forms, and a search DSL designed around relevance tuning. Once search becomes less about "did it match" and more about "why did these ten results win," Elasticsearch usually has more expressive room.&lt;/p&gt;

&lt;p&gt;Elasticsearch also has a clear bias toward denormalized documents. Its join field documentation explicitly warns against modeling multiple levels of relations to replicate a relational schema and recommends denormalization for better search performance. That design choice explains a lot of Elasticsearch's strengths and frustrations. It is not trying to be PostgreSQL with a faster &lt;code&gt;LIKE&lt;/code&gt;. It is trying to be a search engine that can score and retrieve large document collections quickly.&lt;/p&gt;

&lt;h2&gt;
  
  
  PostgreSQL full text search vs Elasticsearch on real features
&lt;/h2&gt;

&lt;p&gt;Typo tolerance is where the two systems diverge sharply. Elasticsearch provides fuzzy queries based on Levenshtein edit distance and also offers dedicated suggestion and as-you-type field types. PostgreSQL native full text search is not typo tolerant by itself. The usual PostgreSQL answer is &lt;code&gt;pg_trgm&lt;/code&gt;, which adds similarity operators and index support for trigram similarity, &lt;code&gt;LIKE&lt;/code&gt;, and &lt;code&gt;ILIKE&lt;/code&gt;. That works well, but it is a composition strategy rather than one integrated search engine feature set.&lt;/p&gt;

&lt;p&gt;Highlighting exists in both stacks, but the implementation details tell a story. PostgreSQL uses &lt;code&gt;ts_headline&lt;/code&gt;, which can return useful snippets, yet the docs note that it uses the original document, can be slow, and is not guaranteed safe for direct insertion into web pages. Elasticsearch highlighting can use postings offsets or term vectors, which is especially valuable on large fields because it avoids reanalyzing the full text for every highlight request. In short, PostgreSQL can highlight, while Elasticsearch is built to highlight at scale.&lt;/p&gt;

&lt;p&gt;Facets and search analytics are another fault line. Elasticsearch treats aggregations as a first-class part of the search model, with metric, bucket, and pipeline aggregations available directly in the search response. PostgreSQL can obviously aggregate because it is SQL, but once counted buckets, histograms, and composable search analytics become part of the search product itself, Elasticsearch feels much more native. The difference is not capability in principle. It is how much query ergonomics and performance policy the engine dedicates to that workload.&lt;/p&gt;

&lt;p&gt;Autocomplete follows the same pattern. PostgreSQL can do prefix matching in &lt;code&gt;to_tsquery&lt;/code&gt;, which is useful and often enough for internal tools. Elasticsearch goes further with &lt;code&gt;search_as_you_type&lt;/code&gt; fields that automatically build multiple analyzed subfields for prefix and infix completion, plus completion suggesters that are purpose-built for fast suggestions. That gap is minor on an admin panel and major on a user-facing discovery surface.&lt;/p&gt;

&lt;h2&gt;
  
  
  Operational cost matters more than benchmark screenshots
&lt;/h2&gt;

&lt;p&gt;The tempting search-engine question is "Is Elasticsearch faster than PostgreSQL for search?" The honest answer is "for what shape of search?" Elasticsearch is engineered around shards, replicas, bulk indexing, refresh policy, and lifecycle management. Elastic's own production docs go deep on shard strategy, bulk request sizing, indexing throughput, refresh intervals, and ILM. PostgreSQL avoids a second cluster, but GIN maintenance is not free. PostgreSQL's docs warn that GIN inserts can be slow, that pending-list cleanup can cause response-time fluctuations, and that autovacuum strategy matters if the index is updated heavily.&lt;/p&gt;

&lt;p&gt;That makes the performance story more nuanced than most comparison posts admit. Elasticsearch usually has more headroom for large top-N lexical search, faceting, autocomplete, and distributed read volume because its architecture is dedicated to those tasks. PostgreSQL often feels faster for relational application queries with strict freshness requirements because there is no second datastore, no refresh boundary, and no sync path to debug. The winner is usually the workload shape, not the benchmark screenshot. That is partly an inference, but it follows directly from PostgreSQL's transactional MVCC model and Elasticsearch's near-real-time shard-based design.&lt;/p&gt;

&lt;p&gt;Should transactional data and search indexes live in the same system? When search relevance is modest but freshness, permissions, and transactional truth are critical, the same-system design has obvious advantages. When search quality, faceting, synonym policy, typo tolerance, and horizontal search scale become first-class product concerns, a second system starts to look justified. Elasticsearch's own shard-sizing guidance says there is no one-size-fits-all strategy and recommends benchmarking production data on production hardware. That sentence captures the trade perfectly. Elasticsearch buys headroom by asking you to operate more search-specific architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  The practical verdict
&lt;/h2&gt;

&lt;p&gt;PostgreSQL full text search wins the first 80 percent surprisingly often. It supports tokenization, stop words, stemming, phrase queries, weights, ranking, highlighting, generated search vectors, GIN indexes, and trigram-based similarity helpers. Combined with PostgreSQL's transactional semantics, it gives many applications a search stack that is simple, current, and close to the data. For SaaS back offices, internal tools, moderate content sites, and app-native search, that combination is hard to dismiss.&lt;/p&gt;

&lt;p&gt;Elasticsearch becomes persuasive when search is not merely a filter but a product surface. BM25 by default, custom analyzers, synonym filters, fuzzy queries, multi-field ranking, aggregations, dedicated autocomplete options, large-field highlighting strategies, and distributed shard-based scaling are not side features. They are the reason the engine exists. That is why Elasticsearch comparisons that focus only on raw latency usually miss the point. The bigger difference is how much search product logic the engine is willing to own.&lt;/p&gt;

&lt;p&gt;The cleanest mental model is this. PostgreSQL full text search is excellent when search belongs to the database. Elasticsearch is excellent when the database must feed a search platform. Most teams over-focus on speed and under-focus on failure modes. The real trade is where relevance tuning, data freshness, and operational complexity are allowed to reside.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>architecture</category>
      <category>database</category>
      <category>sql</category>
    </item>
    <item>
      <title>Chat Platforms as System Interfaces in Modern Systems</title>
      <dc:creator>Rost</dc:creator>
      <pubDate>Wed, 15 Apr 2026 14:17:08 +0000</pubDate>
      <link>https://dev.to/rosgluk/chat-platforms-as-system-interfaces-in-modern-systems-494c</link>
      <guid>https://dev.to/rosgluk/chat-platforms-as-system-interfaces-in-modern-systems-494c</guid>
      <description>&lt;p&gt;Chat platforms have evolved far beyond messaging tools.&lt;br&gt;
In modern systems they operate as interfaces between automated processes and human decision making.&lt;/p&gt;



&lt;p&gt;Slack and Discord are often treated as notification sinks.&lt;br&gt;
In practice they behave more like control surfaces where alerts become actions and messages become events.&lt;/p&gt;

&lt;p&gt;The shift is subtle but important.&lt;br&gt;
Systems are no longer observed only through dashboards,&lt;br&gt;
they are interacted with directly through chat.&lt;/p&gt;


&lt;h2&gt;
  
  
  Chat as an Interface Layer
&lt;/h2&gt;

&lt;p&gt;Chat platforms sit between system signals and human actions.&lt;/p&gt;
&lt;h3&gt;
  
  
  Notification Layer
&lt;/h3&gt;

&lt;p&gt;Systems emit signals such as alerts logs and state changes. These are delivered into chat channels where they become visible to teams.&lt;/p&gt;
&lt;h3&gt;
  
  
  Interaction Layer
&lt;/h3&gt;

&lt;p&gt;Users respond through commands buttons or reactions. These interactions are structured inputs that can be consumed by backend systems.&lt;/p&gt;
&lt;h3&gt;
  
  
  Control Layer
&lt;/h3&gt;

&lt;p&gt;Chat becomes a mechanism for triggering behavior. Deployments can be approved services restarted and workflows executed without leaving the interface.&lt;/p&gt;

&lt;p&gt;This layered model turns chat into a system boundary rather than a passive endpoint.&lt;/p&gt;


&lt;h2&gt;
  
  
  Architecture Perspective
&lt;/h2&gt;

&lt;p&gt;A simplified model looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Systems -&amp;gt; Events -&amp;gt; Chat Platform -&amp;gt; Human -&amp;gt; Action -&amp;gt; Systems
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The platform acts as a bridge between automation and decision making. It enables a feedback loop where humans influence system behavior in real time.&lt;/p&gt;




&lt;h2&gt;
  
  
  Patterns of Chat Based Systems
&lt;/h2&gt;

&lt;p&gt;Several recurring patterns appear when chat is used as an interface.&lt;/p&gt;

&lt;h3&gt;
  
  
  Alerting Interfaces
&lt;/h3&gt;

&lt;p&gt;Alerts are routed into channels where teams can observe and react. The value is not only visibility but shared context.&lt;/p&gt;

&lt;h3&gt;
  
  
  Workflow Interfaces
&lt;/h3&gt;

&lt;p&gt;Slack in particular enables structured workflows. Tasks can be assigned approved or escalated through defined interactions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Control Interfaces
&lt;/h3&gt;

&lt;p&gt;Commands and reactions trigger system actions. This is common in deployment pipelines and operational tooling.&lt;/p&gt;

&lt;h3&gt;
  
  
  Monitoring Interfaces
&lt;/h3&gt;

&lt;p&gt;Chat provides a lightweight view into system state. Instead of dashboards users receive curated signals in context.&lt;/p&gt;




&lt;h2&gt;
  
  
  Slack and Discord as System Roles
&lt;/h2&gt;

&lt;p&gt;Both platforms support similar primitives but lead to different system designs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Slack
&lt;/h3&gt;

&lt;p&gt;Slack emphasizes structure. Block based messages buttons and integrations enable workflow driven systems, as detailed in &lt;a href="https://www.glukhov.org/app-architecture/integration-patterns/slack/" rel="noopener noreferrer"&gt;Slack patterns for alerts and workflow automation&lt;/a&gt;. It is well suited for coordination and enterprise environments.&lt;/p&gt;

&lt;h3&gt;
  
  
  Discord
&lt;/h3&gt;

&lt;p&gt;Discord favors interaction. Reactions and flexible message handling make it effective for event driven control, which aligns with &lt;a href="https://www.glukhov.org/app-architecture/integration-patterns/discord/" rel="noopener noreferrer"&gt;Discord integration patterns for alerts and control loops&lt;/a&gt;. It is often used in more experimental or highly interactive setups.&lt;/p&gt;

&lt;p&gt;The difference is not capability but orientation. Slack organizes workflows. Discord enables events.&lt;/p&gt;




&lt;h2&gt;
  
  
  When Chat Platforms Fit
&lt;/h2&gt;

&lt;p&gt;Chat platforms work well when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;human decisions are required&lt;/li&gt;
&lt;li&gt;collaboration improves outcomes&lt;/li&gt;
&lt;li&gt;signals are meaningful but not critical&lt;/li&gt;
&lt;li&gt;workflows benefit from visibility&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They are particularly useful in systems where automation and human judgment intersect.&lt;/p&gt;




&lt;h2&gt;
  
  
  When Chat Platforms Do Not Fit
&lt;/h2&gt;

&lt;p&gt;They are less effective when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;alerts require immediate paging&lt;/li&gt;
&lt;li&gt;signals are too frequent&lt;/li&gt;
&lt;li&gt;actions must be fully automated&lt;/li&gt;
&lt;li&gt;strict reliability guarantees are needed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In these cases dedicated systems such as paging services or queues are more appropriate, and teams should rely on &lt;a href="https://www.glukhov.org/observability/alerting/" rel="noopener noreferrer"&gt;modern alerting system design for observability operations&lt;/a&gt; for critical escalation paths.&lt;/p&gt;




&lt;h2&gt;
  
  
  Relationship with Observability
&lt;/h2&gt;

&lt;p&gt;Observability systems generate signals. Chat platforms distribute and operationalize them.&lt;/p&gt;

&lt;p&gt;The distinction matters. Observability answers what is happening. Chat enables what to do next.&lt;/p&gt;

&lt;p&gt;This separation keeps systems clear. Alert design belongs to observability, with &lt;a href="https://www.glukhov.org/observability/alerting/" rel="noopener noreferrer"&gt;alert routing and noise reduction practices&lt;/a&gt; defining signal quality. Interaction belongs to integration patterns.&lt;/p&gt;




&lt;h2&gt;
  
  
  Human in the Loop Systems
&lt;/h2&gt;

&lt;p&gt;Modern systems increasingly rely on human input at key decision points.&lt;/p&gt;

&lt;p&gt;Chat platforms enable this by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;presenting context rich alerts&lt;/li&gt;
&lt;li&gt;allowing immediate responses&lt;/li&gt;
&lt;li&gt;triggering controlled actions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result is a feedback loop where systems and humans operate together rather than separately.&lt;/p&gt;




&lt;h2&gt;
  
  
  Design Considerations
&lt;/h2&gt;

&lt;p&gt;Effective chat based systems require careful design.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;messages must be actionable&lt;/li&gt;
&lt;li&gt;ownership must be clear&lt;/li&gt;
&lt;li&gt;noise must be controlled&lt;/li&gt;
&lt;li&gt;interactions must be safe and idempotent&lt;/li&gt;
&lt;li&gt;security must be enforced&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without these constraints chat becomes a source of noise rather than clarity.&lt;/p&gt;




&lt;h2&gt;
  
  
  Common Anti Patterns
&lt;/h2&gt;

&lt;p&gt;Several mistakes appear frequently.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;treating chat as a message queue&lt;/li&gt;
&lt;li&gt;sending all signals without filtering&lt;/li&gt;
&lt;li&gt;lacking ownership for alerts&lt;/li&gt;
&lt;li&gt;mixing logs with actionable alerts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These reduce signal quality and degrade trust in the system.&lt;/p&gt;




&lt;h2&gt;
  
  
  Positioning in System Architecture
&lt;/h2&gt;

&lt;p&gt;Chat platforms are not monitoring systems and not infrastructure primitives.&lt;/p&gt;

&lt;p&gt;They are interface layers that connect humans to systems.&lt;/p&gt;

&lt;p&gt;This role becomes more important as systems grow more complex and require coordinated responses.&lt;br&gt;
If you are deciding how this interface layer fits with service boundaries and persistence choices, &lt;a href="https://www.glukhov.org/app-architecture/" rel="noopener noreferrer"&gt;this app architecture overview&lt;/a&gt; provides the broader production context.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Chat platforms reshape how systems are operated. They transform alerts into interactions and workflows into conversations.&lt;/p&gt;

&lt;p&gt;Used carefully they provide a powerful bridge between automation and human judgment.&lt;/p&gt;

</description>
      <category>integration</category>
      <category>observability</category>
      <category>architecture</category>
      <category>dev</category>
    </item>
    <item>
      <title>Slack Integration Patterns for Alerts and Workflows</title>
      <dc:creator>Rost</dc:creator>
      <pubDate>Wed, 15 Apr 2026 14:17:07 +0000</pubDate>
      <link>https://dev.to/rosgluk/slack-integration-patterns-for-alerts-and-workflows-1ij5</link>
      <guid>https://dev.to/rosgluk/slack-integration-patterns-for-alerts-and-workflows-1ij5</guid>
      <description>&lt;p&gt;Slack integrations look deceptively easy because you can post a message in one HTTP call.&lt;br&gt;
The interesting part starts when you want Slack to be interactive and reliable.&lt;/p&gt;



&lt;p&gt;This deep dive treats Slack as three different integration surfaces:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Notification sink for one way alerts via incoming webhooks.&lt;/li&gt;
&lt;li&gt;Workflow engine via Workflow Builder and custom workflow steps.&lt;/li&gt;
&lt;li&gt;Event interface via Block Kit buttons, slash commands, and action payloads.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This page describes how systems cross the boundary into a shared UI that can also emit events back into your architecture, not about alert philosophy.&lt;br&gt;
For alert strategy and routing, see &lt;a href="https://www.glukhov.org/observability/alerting/" rel="noopener noreferrer"&gt;Modern Alerting Systems Design for Observability Teams&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Related reading:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.glukhov.org/app-architecture/integration-patterns/chat-platforms-as-system-interfaces/" rel="noopener noreferrer"&gt;Chat Platforms as System Interfaces in Modern Systems&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.glukhov.org/app-architecture/integration-patterns/discord/" rel="noopener noreferrer"&gt;Discord Integration Pattern for Alerts and Control Loops&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.glukhov.org/observability/alerting/" rel="noopener noreferrer"&gt;Modern Alerting Systems Design for Observability Teams&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Canonical framing and placement in integration patterns
&lt;/h2&gt;

&lt;p&gt;Slack is not just where alerts go to die. Used well, Slack becomes a system interface where messages are stateful artifacts and user interactions are events.&lt;/p&gt;

&lt;p&gt;This page is canonically placed under /app-architecture/integration-patterns/slack/ because the main question is not "should we alert" but "what is the contract between our system and Slack".&lt;/p&gt;

&lt;p&gt;If your solution requires any of the following, you are in integration-pattern territory, not simple notification territory:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A decision loop, where a human approval gates an action.&lt;/li&gt;
&lt;li&gt;A workflow, where Slack collects context and triggers steps.&lt;/li&gt;
&lt;li&gt;An event loop, where Slack emits actions that your system subscribes to.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Slack platform intentionally supports both one way messaging and bidirectional interaction through request URLs and interaction payloads. Incoming webhooks are a first class way to post JSON payloads, including Block Kit building blocks, to a channel (&lt;a href="https://docs.slack.dev/messaging/sending-messages-using-incoming-webhooks/" rel="noopener noreferrer"&gt;Sending messages using incoming webhooks&lt;/a&gt;). Interactivity is delivered back to your app as HTTP POST requests to a configured Request URL, and those payloads are form encoded with a JSON payload field (&lt;a href="https://docs.slack.dev/interactivity/handling-user-interaction/" rel="noopener noreferrer"&gt;Handling user interaction&lt;/a&gt;).&lt;/p&gt;
&lt;h3&gt;
  
  
  Slack as a notification sink
&lt;/h3&gt;

&lt;p&gt;Incoming webhooks are the fastest path to value for alerts and status updates. A webhook is a unique URL tied to an app installation, and you POST a JSON message to it (&lt;a href="https://docs.slack.dev/messaging/sending-messages-using-incoming-webhooks/" rel="noopener noreferrer"&gt;Sending messages using incoming webhooks&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Opinionated take: webhooks are an excellent default when you want deliver-and-forget messages and you do not need Slack to be a control surface. Webhooks are also an excellent way to decouple your onboarding from your eventual app architecture.&lt;/p&gt;
&lt;h3&gt;
  
  
  Slack as a workflow engine
&lt;/h3&gt;

&lt;p&gt;Workflow Builder exists because chat is where work actually happens. Workflows can be simple or complex and can connect to apps (&lt;a href="https://slack.com/help/articles/360035692513-Guide-to-Slack-Workflow-Builder" rel="noopener noreferrer"&gt;Guide to Workflow Builder&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Custom workflow steps let you expose your systems as reusable building blocks inside Workflow Builder (&lt;a href="https://docs.slack.dev/workflows/workflow-steps/" rel="noopener noreferrer"&gt;Workflow steps&lt;/a&gt;). This is a different integration shape than bots in channels. It moves your integration closer to "tooling inside Slack" than "messages from the outside".&lt;/p&gt;

&lt;p&gt;Opinionated take: if your organization already thinks in workflows and approvals, workflow steps can feel more native than bespoke bots.&lt;/p&gt;
&lt;h3&gt;
  
  
  Slack as an event interface
&lt;/h3&gt;

&lt;p&gt;Block Kit turns a message into a UI surface (&lt;a href="https://docs.slack.dev/block-kit/" rel="noopener noreferrer"&gt;Block Kit&lt;/a&gt;). Interactive components like buttons generate action payloads, typically block_actions payloads, that are sent to your app when a user clicks (&lt;a href="https://docs.slack.dev/reference/interaction-payloads/block_actions-payload/" rel="noopener noreferrer"&gt;block_actions payload&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Buttons have explicit identifiers action_id and optional value, and must be hosted inside section or actions blocks (&lt;a href="https://docs.slack.dev/reference/block-kit/block-elements/button-element/" rel="noopener noreferrer"&gt;Button element&lt;/a&gt;). When you design a message with a button, you are designing an event source.&lt;/p&gt;

&lt;p&gt;This is where FAQ topics like request verification, required scopes, and safe internal triggers become the center of the design.&lt;/p&gt;
&lt;h2&gt;
  
  
  Architecture patterns that scale
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Webhook flow for one way alerting
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[service] -&amp;gt; [alert formatter] -&amp;gt; [slack incoming webhook] -&amp;gt; [channel]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Incoming webhooks accept JSON payloads and support Block Kit layouts (&lt;a href="https://docs.slack.dev/messaging/sending-messages-using-incoming-webhooks/" rel="noopener noreferrer"&gt;Sending messages using incoming webhooks&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;When the FAQ asks about the fastest way to send alerts, this is usually it.&lt;/p&gt;
&lt;h3&gt;
  
  
  Brokered flow with a queue for reliability and backpressure
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[services] -&amp;gt; [queue topic] -&amp;gt; [slack dispatcher] -&amp;gt; [slack api]
                     |                 |
                     |                 +-&amp;gt; [rate limit handler]
                     +-&amp;gt; [dead letter queue]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Slack rate limits apply to HTTP based APIs, including incoming webhooks, and Slack returns HTTP 429 with a Retry-After header when you exceed limits (&lt;a href="https://docs.slack.dev/apis/web-api/rate-limits/" rel="noopener noreferrer"&gt;Rate limits&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Opinionated take: if you post alerts directly from every service, the first incident turns into a distributed denial of service against your own Slack integration. A dispatcher behind a queue tends to be a calmer architecture.&lt;/p&gt;
&lt;h3&gt;
  
  
  Workflow automation pattern with approvals
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[alert] -&amp;gt; [slack message with button] -&amp;gt; [button click]
   -&amp;gt; [action payload] -&amp;gt; [approval handler] -&amp;gt; [internal API] -&amp;gt; [update message]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Slack interactivity requires configuring a Request URL and enabling Interactivity. Slack sends interaction payloads as application/x-www-form-urlencoded with a payload parameter that contains JSON, and you must respond with HTTP 200 within 3 seconds (&lt;a href="https://docs.slack.dev/interactivity/handling-user-interaction/" rel="noopener noreferrer"&gt;Handling user interaction&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;This is the pattern behind the FAQ item about triggering internal actions safely.&lt;/p&gt;
&lt;h3&gt;
  
  
  Slack interaction flowchart diagram
&lt;/h3&gt;
&lt;h2&gt;
  
  
  Webhook vs app and the implementation mechanics
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Recommended libraries
&lt;/h3&gt;

&lt;p&gt;Go:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;slack-go/slack for Web API and Block Kit structures (&lt;a href="https://github.com/slack-go/slack" rel="noopener noreferrer"&gt;slack-go/slack repo&lt;/a&gt;, &lt;a href="https://pkg.go.dev/github.com/slack-go/slack" rel="noopener noreferrer"&gt;pkg.go.dev docs&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Python:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;slack_sdk (Python Slack SDK) for Web API clients, signature helpers, and retry infrastructure (&lt;a href="https://docs.slack.dev/tools/python-slack-sdk/" rel="noopener noreferrer"&gt;Python Slack SDK docs&lt;/a&gt;, &lt;a href="https://github.com/slackapi/python-slack-sdk" rel="noopener noreferrer"&gt;python-slack-sdk repo&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Webhook vs app bot approach
&lt;/h3&gt;

&lt;p&gt;A practical comparison:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Capability&lt;/th&gt;
&lt;th&gt;Incoming webhook&lt;/th&gt;
&lt;th&gt;Slack app with bot token&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Post messages&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Post Block Kit layouts&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Receive button clicks&lt;/td&gt;
&lt;td&gt;Only if tied to an app with interactivity&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Slash commands&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Workflow steps&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Security surface&lt;/td&gt;
&lt;td&gt;Webhook URL secrecy&lt;/td&gt;
&lt;td&gt;OAuth tokens plus signing secret&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best fit&lt;/td&gt;
&lt;td&gt;One way alerts&lt;/td&gt;
&lt;td&gt;Workflows, approvals, interactive UI&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Slack explicitly supports Block Kit layouts with incoming webhooks (&lt;a href="https://docs.slack.dev/messaging/sending-messages-using-incoming-webhooks/" rel="noopener noreferrer"&gt;Sending messages using incoming webhooks&lt;/a&gt;). Interactivity is configured per app and delivered to a Request URL (&lt;a href="https://docs.slack.dev/interactivity/handling-user-interaction/" rel="noopener noreferrer"&gt;Handling user interaction&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Opinionated take: webhooks are a great first milestone, but as soon as you want Slack to be a control surface, you are building an app. Avoid pretending otherwise.&lt;/p&gt;
&lt;h3&gt;
  
  
  Scopes and permissions
&lt;/h3&gt;

&lt;p&gt;Slack scopes define what your app can do. There is a central scopes reference and individual scope pages (&lt;a href="https://docs.slack.dev/reference/scopes/" rel="noopener noreferrer"&gt;Scopes reference&lt;/a&gt;). For sending messages via Web API, chat:write is the canonical scope (&lt;a href="https://docs.slack.dev/reference/scopes/chat.write/" rel="noopener noreferrer"&gt;chat:write scope&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;For slash commands, you typically need the commands scope and a configured command request URL (commands are part of interactivity docs, and each command has its own Request URL) (&lt;a href="https://docs.slack.dev/interactivity/handling-user-interaction/" rel="noopener noreferrer"&gt;Handling user interaction&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;FAQ note: button payload delivery is not "a scope", it is an app setting. Your app receives payloads when Interactivity is enabled and Request URL is set, but posting message updates still generally requires chat:write.&lt;/p&gt;
&lt;h3&gt;
  
  
  Rate limits and retries
&lt;/h3&gt;

&lt;p&gt;Slack rate limits return HTTP 429 and include Retry-After in seconds, and this applies to HTTP based APIs including incoming webhooks (&lt;a href="https://docs.slack.dev/apis/web-api/rate-limits/" rel="noopener noreferrer"&gt;Rate limits&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;In practice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;honor Retry-After&lt;/li&gt;
&lt;li&gt;apply backoff with jitter for transient 5xx&lt;/li&gt;
&lt;li&gt;centralize Slack delivery in a dispatcher when volume grows&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Idempotency and deduplication
&lt;/h3&gt;

&lt;p&gt;Slack expects an acknowledgment for interaction payloads within 3 seconds, otherwise users see an error and Slack may retry delivery behavior depending on feature (&lt;a href="https://docs.slack.dev/interactivity/handling-user-interaction/" rel="noopener noreferrer"&gt;Handling user interaction&lt;/a&gt;). For the Events API, Slack explicitly provides retry metadata headers x-slack-retry-num (&lt;a href="https://docs.slack.dev/apis/events-api/" rel="noopener noreferrer"&gt;Events API&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Even without explicit retries, duplicates happen because users double-click and because distributed systems retransmit. If your button triggers an internal action, treat clicks as at least once events and dedupe.&lt;/p&gt;

&lt;p&gt;A practical idempotency strategy for approvals:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;idempotency key = team_id + channel_id + message_ts + action_id + user_id&lt;/li&gt;
&lt;li&gt;store key in Redis with TTL matching your workflow window&lt;/li&gt;
&lt;li&gt;internal action API also enforces idempotency, not only the Slack handler&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Security fundamentals and request verification
&lt;/h3&gt;

&lt;p&gt;Slack signs requests to your server using your app signing secret. Slack sends X-Slack-Signature and X-Slack-Request-Timestamp headers, and Slack recommends rejecting requests older than five minutes to prevent replay attacks (&lt;a href="https://docs.slack.dev/authentication/verifying-requests-from-slack/" rel="noopener noreferrer"&gt;Verifying requests from Slack&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Two footguns that show up in real code reviews:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You must compute the signature over the raw request body, before JSON parsing or form decoding (&lt;a href="https://docs.slack.dev/authentication/verifying-requests-from-slack/" rel="noopener noreferrer"&gt;Verifying requests from Slack&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;You must ack interactive payloads within 3 seconds, so do heavy work asynchronously and use response_url to communicate results (&lt;a href="https://docs.slack.dev/interactivity/handling-user-interaction/" rel="noopener noreferrer"&gt;Handling user interaction&lt;/a&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Python Slack SDK includes a request signature verifier utility in code and docs (&lt;a href="https://github.com/slackapi/python-slack-sdk/blob/main/slack_sdk/signature/__init__.py" rel="noopener noreferrer"&gt;python-slack-sdk signature verifier&lt;/a&gt;).&lt;/p&gt;
&lt;h2&gt;
  
  
  Message and interaction design
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Alert message template
&lt;/h3&gt;

&lt;p&gt;If you want Slack to act like a system interface, structure your messages so decisions are obvious. A message template that works well across teams:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;title&lt;/li&gt;
&lt;li&gt;severity&lt;/li&gt;
&lt;li&gt;context&lt;/li&gt;
&lt;li&gt;action hint&lt;/li&gt;
&lt;li&gt;links&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A minimal template:&lt;/p&gt;

&lt;p&gt;title: checkout error rate elevated&lt;br&gt;
severity: warn&lt;br&gt;
context: service=checkout env=prod region=us-east&lt;br&gt;
action_hint: click Approve restart to trigger a safe restart&lt;/p&gt;
&lt;h3&gt;
  
  
  Incoming webhook payload example
&lt;/h3&gt;

&lt;p&gt;Incoming webhooks accept JSON payloads and can include rich layouts using Block Kit (&lt;a href="https://docs.slack.dev/messaging/sending-messages-using-incoming-webhooks/" rel="noopener noreferrer"&gt;Sending messages using incoming webhooks&lt;/a&gt;).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"checkout error rate elevated"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"blocks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"header"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"plain_text"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"checkout error rate elevated"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"section"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"fields"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mrkdwn"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*severity*&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;nwarn"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mrkdwn"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*context*&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;nservice=checkout env=prod region=us-east"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"section"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mrkdwn"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*action_hint*&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;nClick Approve to trigger a safe restart."&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Designing buttons and identifiers
&lt;/h3&gt;

&lt;p&gt;Buttons must be inside section or actions blocks and include action_id and optional value (&lt;a href="https://docs.slack.dev/reference/block-kit/block-elements/button-element/" rel="noopener noreferrer"&gt;Button element&lt;/a&gt;). action_id is your routing key. value is your payload. Together, they are your event schema.&lt;/p&gt;

&lt;p&gt;Opinionated take: choose action_id values like stable API endpoints. Names like "approve_restart" age better than "button_1".&lt;/p&gt;

&lt;h3&gt;
  
  
  Interaction payload handling, response_url, and timing
&lt;/h3&gt;

&lt;p&gt;Slack sends interaction payloads to your Request URL as form encoded data with a payload parameter containing JSON. The payload includes a type field defining the source, such as block_actions for button clicks (&lt;a href="https://docs.slack.dev/interactivity/handling-user-interaction/" rel="noopener noreferrer"&gt;Handling user interaction&lt;/a&gt;, &lt;a href="https://docs.slack.dev/reference/interaction-payloads/" rel="noopener noreferrer"&gt;Interaction payloads&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;You must return HTTP 200 within 3 seconds for the acknowledgment response (&lt;a href="https://docs.slack.dev/interactivity/handling-user-interaction/" rel="noopener noreferrer"&gt;Handling user interaction&lt;/a&gt;). Use response_url to update the original message or respond in-channel or in a thread, and Slack limits response_url usage to up to five times within thirty minutes (&lt;a href="https://docs.slack.dev/interactivity/handling-user-interaction/" rel="noopener noreferrer"&gt;Handling user interaction&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;This timing constraint is a design constraint. It forces you to decouple "acknowledge" from "do work".&lt;/p&gt;

&lt;h3&gt;
  
  
  Interaction patterns that fit Slack
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Buttons in Block Kit for approvals and branching.&lt;/li&gt;
&lt;li&gt;Slash commands for explicit user intent and parameters.&lt;/li&gt;
&lt;li&gt;Workflow steps for repeatable business processes in Workflow Builder (&lt;a href="https://docs.slack.dev/workflows/workflow-steps/" rel="noopener noreferrer"&gt;Workflow steps&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;Shortcuts and modals when you need structured input, with trigger_id constraints described in interactivity docs (&lt;a href="https://docs.slack.dev/interactivity/handling-user-interaction/" rel="noopener noreferrer"&gt;Handling user interaction&lt;/a&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Go and Python examples
&lt;/h2&gt;

&lt;p&gt;Publisher note: these can be split into dedicated example pages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/app-architecture/integration-patterns/slack/go-example&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/app-architecture/integration-patterns/slack/python-example&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The examples prioritize one thing that keeps systems stable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;verify Slack signatures&lt;/li&gt;
&lt;li&gt;ack within three seconds&lt;/li&gt;
&lt;li&gt;dedupe actions&lt;/li&gt;
&lt;li&gt;trigger an internal HTTP POST&lt;/li&gt;
&lt;li&gt;optionally update Slack using response_url&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Go example send alert and handle button approval
&lt;/h3&gt;

&lt;p&gt;Prereqs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Slack app with Interactivity enabled and Request URL configured (&lt;a href="https://docs.slack.dev/interactivity/handling-user-interaction/" rel="noopener noreferrer"&gt;Handling user interaction&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;Bot token with chat:write scope (&lt;a href="https://docs.slack.dev/reference/scopes/chat.write/" rel="noopener noreferrer"&gt;chat:write scope&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;A signing secret for request verification (&lt;a href="https://docs.slack.dev/authentication/verifying-requests-from-slack/" rel="noopener noreferrer"&gt;Verifying requests from Slack&lt;/a&gt;).
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;package&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s"&gt;"bytes"&lt;/span&gt;
  &lt;span class="s"&gt;"crypto/hmac"&lt;/span&gt;
  &lt;span class="s"&gt;"crypto/sha256"&lt;/span&gt;
  &lt;span class="s"&gt;"encoding/hex"&lt;/span&gt;
  &lt;span class="s"&gt;"encoding/json"&lt;/span&gt;
  &lt;span class="s"&gt;"io"&lt;/span&gt;
  &lt;span class="s"&gt;"log"&lt;/span&gt;
  &lt;span class="s"&gt;"net/http"&lt;/span&gt;
  &lt;span class="s"&gt;"net/url"&lt;/span&gt;
  &lt;span class="s"&gt;"os"&lt;/span&gt;
  &lt;span class="s"&gt;"strconv"&lt;/span&gt;
  &lt;span class="s"&gt;"strings"&lt;/span&gt;
  &lt;span class="s"&gt;"sync"&lt;/span&gt;
  &lt;span class="s"&gt;"time"&lt;/span&gt;

  &lt;span class="s"&gt;"github.com/slack-go/slack"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;BlockActionPayload&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;Type&lt;/span&gt;  &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`json:"type"`&lt;/span&gt;
  &lt;span class="n"&gt;Team&lt;/span&gt;  &lt;span class="k"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;ID&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`json:"id"`&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="s"&gt;`json:"team"`&lt;/span&gt;
  &lt;span class="n"&gt;User&lt;/span&gt;  &lt;span class="k"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;ID&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`json:"id"`&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="s"&gt;`json:"user"`&lt;/span&gt;
  &lt;span class="n"&gt;Channel&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ID&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`json:"id"`&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="s"&gt;`json:"channel"`&lt;/span&gt;
  &lt;span class="n"&gt;Message&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Ts&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`json:"ts"`&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="s"&gt;`json:"message"`&lt;/span&gt;
  &lt;span class="n"&gt;ResponseURL&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`json:"response_url"`&lt;/span&gt;
  &lt;span class="n"&gt;Actions&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ActionID&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`json:"action_id"`&lt;/span&gt;
    &lt;span class="n"&gt;Value&lt;/span&gt;    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`json:"value"`&lt;/span&gt;
    &lt;span class="n"&gt;Type&lt;/span&gt;     &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`json:"type"`&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="s"&gt;`json:"actions"`&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;InternalAction&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;Action&lt;/span&gt;     &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`json:"action"`&lt;/span&gt;
  &lt;span class="n"&gt;TeamID&lt;/span&gt;     &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`json:"team_id"`&lt;/span&gt;
  &lt;span class="n"&gt;ChannelID&lt;/span&gt;  &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`json:"channel_id"`&lt;/span&gt;
  &lt;span class="n"&gt;MessageTS&lt;/span&gt;  &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`json:"message_ts"`&lt;/span&gt;
  &lt;span class="n"&gt;UserID&lt;/span&gt;     &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`json:"user_id"`&lt;/span&gt;
  &lt;span class="n"&gt;Value&lt;/span&gt;      &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`json:"value"`&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="c"&gt;// In production, store this in Redis with TTL&lt;/span&gt;
  &lt;span class="n"&gt;seenMu&lt;/span&gt; &lt;span class="n"&gt;sync&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Mutex&lt;/span&gt;
  &lt;span class="n"&gt;seen&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt;
  &lt;span class="n"&gt;ttl&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Minute&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;botToken&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"SLACK_BOT_TOKEN"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;signingSecret&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"SLACK_SIGNING_SECRET"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;channelID&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"SLACK_CHANNEL_ID"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;internalURL&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"INTERNAL_API_URL"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;listenAddr&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"LISTEN_ADDR"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// e.g. :8080&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;botToken&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;signingSecret&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;channelID&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;internalURL&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;listenAddr&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fatal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"missing env vars SLACK_BOT_TOKEN SLACK_SIGNING_SECRET SLACK_CHANNEL_ID INTERNAL_API_URL LISTEN_ADDR"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="n"&gt;api&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;slack&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;botToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="c"&gt;// Send an alert message with an approval button.&lt;/span&gt;
  &lt;span class="c"&gt;// Buttons are interactive Block Kit elements with action_id and value.&lt;/span&gt;
  &lt;span class="c"&gt;// See Slack Block Kit button element docs.&lt;/span&gt;
  &lt;span class="n"&gt;blocks&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;slack&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Blocks&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;BlockSet&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;slack&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Block&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;slack&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewHeaderBlock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;slack&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewTextBlockObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"plain_text"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"checkout error rate elevated"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
      &lt;span class="n"&gt;slack&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewSectionBlock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;slack&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewTextBlockObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"mrkdwn"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"*severity*&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s"&gt;nwarn&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s"&gt;n*context*&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s"&gt;nservice=checkout env=prod"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="n"&gt;slack&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewActionBlock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s"&gt;"actions_1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;slack&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewButtonBlockElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"approve_restart"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"restart"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;slack&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewTextBlockObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"plain_text"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Approve restart"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
      &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PostMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;channelID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;slack&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MsgOptionBlocks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;blocks&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BlockSet&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fatalf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"PostMessage failed: %v"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"posted alert message_ts=%s"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="c"&gt;// Interactivity endpoint&lt;/span&gt;
  &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HandleFunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/slack/actions"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ResponseWriter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;rawBody&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReadAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"read body failed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusBadRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="c"&gt;// Verify Slack request signature on the raw body and timestamp.&lt;/span&gt;
    &lt;span class="c"&gt;// See Slack verifying requests docs.&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;verifySlackRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Header&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rawBody&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;signingSecret&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"invalid signature"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusUnauthorized&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c"&gt;// Slack sends application/x-www-form-urlencoded with payload=JSON&lt;/span&gt;
    &lt;span class="n"&gt;vals&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ParseQuery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rawBody&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"bad form body"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusBadRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;payloadStr&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;vals&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"payload"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;payloadStr&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"missing payload"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusBadRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="n"&gt;BlockActionPayload&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Unmarshal&lt;/span&gt;&lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payloadStr&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"bad payload json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusBadRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c"&gt;// Ack within 3 seconds. Do real work async and use response_url for updates.&lt;/span&gt;
    &lt;span class="c"&gt;// See Slack interactivity docs on acknowledgment timing.&lt;/span&gt;
    &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WriteHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusOK&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Write&lt;/span&gt;&lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Type&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s"&gt;"block_actions"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Actions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Actions&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ActionID&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s"&gt;"approve_restart"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="c"&gt;// Dedupe approvals&lt;/span&gt;
      &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Join&lt;/span&gt;&lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Team&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Channel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ActionID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="s"&gt;"|"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;tryOnce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="n"&gt;req&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;InternalAction&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Action&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;    &lt;span class="s"&gt;"approve_restart"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;TeamID&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Team&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;ChannelID&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Channel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;MessageTS&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;UserID&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;     &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;postJSON&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;internalURL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"internal action failed: %v"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;replyViaResponseURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ResponseURL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"action failed, check logs"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;replyViaResponseURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ResponseURL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"approval received, internal action triggered"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}()&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"listening on %s"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;listenAddr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fatal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ListenAndServe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;listenAddr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;tryOnce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="n"&gt;seenMu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Lock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;seenMu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Unlock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;seen&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ttl&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nb"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="n"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;verifySlackRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Header&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;signingSecret&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"X-Slack-Request-Timestamp"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;sig&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"X-Slack-Signature"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;sig&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="n"&gt;tsInt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;strconv&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ParseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c"&gt;// Reject requests older than 5 minutes to reduce replay risk.&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Since&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Unix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tsInt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Minute&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="s"&gt;"v0:"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;":"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;mac&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;hmac&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sha256&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;signingSecret&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="n"&gt;mac&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Write&lt;/span&gt;&lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="n"&gt;sum&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;hex&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EncodeToString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mac&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="n"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="s"&gt;"v0="&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;sum&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;hmac&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Equal&lt;/span&gt;&lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sig&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;postJSON&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Marshal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MethodPost&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewReader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Header&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Content-Type"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"application/json"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Timeout&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Do&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusCode&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="m"&gt;200&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusCode&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="m"&gt;300&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ErrUnexpectedEOF&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;replyViaResponseURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;responseURL&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;responseURL&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="c"&gt;// response_url accepts JSON payloads and can post ephemeral by default.&lt;/span&gt;
  &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Marshal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s"&gt;"text"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MethodPost&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;responseURL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewReader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Header&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Content-Type"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"application/json"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Timeout&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Do&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Python example send alert and handle button approval
&lt;/h3&gt;

&lt;p&gt;Prereqs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Slack app with Interactivity enabled and Request URL configured (&lt;a href="https://docs.slack.dev/interactivity/handling-user-interaction/" rel="noopener noreferrer"&gt;Handling user interaction&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;Bot token with chat:write scope (&lt;a href="https://docs.slack.dev/reference/scopes/chat.write/" rel="noopener noreferrer"&gt;chat:write scope&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;Signing secret for request verification (&lt;a href="https://docs.slack.dev/authentication/verifying-requests-from-slack/" rel="noopener noreferrer"&gt;Verifying requests from Slack&lt;/a&gt;).
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;threading&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;flask&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Flask&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;make_response&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;slack_sdk&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;WebClient&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;slack_sdk.signature&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SignatureVerifier&lt;/span&gt;

&lt;span class="n"&gt;SLACK_BOT_TOKEN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SLACK_BOT_TOKEN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;SLACK_SIGNING_SECRET&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SLACK_SIGNING_SECRET&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;SLACK_CHANNEL_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SLACK_CHANNEL_ID&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;INTERNAL_API_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INTERNAL_API_URL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;WebClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;SLACK_BOT_TOKEN&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;verifier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SignatureVerifier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;signing_secret&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;SLACK_SIGNING_SECRET&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Flask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__name__&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# In production, store these in Redis
&lt;/span&gt;&lt;span class="n"&gt;_seen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="n"&gt;_TTL_SECONDS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;600&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;try_once&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="n"&gt;expired&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;_seen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_TTL_SECONDS&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;expired&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;_seen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;_seen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
    &lt;span class="n"&gt;_seen&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;post_internal_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;INTERNAL_API_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;reply_via_response_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;response_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;send_alert_with_button&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;blocks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;header&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;plain_text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;checkout error rate elevated&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;section&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mrkdwn&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*severity*&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s"&gt;nwarn&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s"&gt;n*context*&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s"&gt;nservice=checkout env=prod&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;actions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;block_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;actions_1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;elements&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;button&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;plain_text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Approve restart&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;action_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;approve_restart&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;restart&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="c1"&gt;# chat.postMessage requires chat:write scope.
&lt;/span&gt;    &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;chat_postMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;SLACK_CHANNEL_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;checkout error rate elevated&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;blocks&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;blocks&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@app.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/slack/actions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;slack_actions&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;raw_body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_data&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# Slack recommends verifying raw body before parsing
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;verifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_valid_request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw_body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;make_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;invalid signature&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Slack sends application/x-www-form-urlencoded with a payload field containing JSON.
&lt;/span&gt;    &lt;span class="n"&gt;payload_str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;payload&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;payload_str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;make_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;missing payload&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload_str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Ack within 3 seconds. Slack user sees errors if you do not.
&lt;/span&gt;    &lt;span class="c1"&gt;# See Slack interactivity docs on acknowledgment.
&lt;/span&gt;    &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;make_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;work&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;block_actions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;
        &lt;span class="n"&gt;actions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;actions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;

        &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;action_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;approve_restart&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;

        &lt;span class="n"&gt;team_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;team&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;channel_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;channel&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;message_ts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;|&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;team_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;channel_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message_ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;approve_restart&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;try_once&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;

        &lt;span class="n"&gt;internal_payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;action&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;approve_restart&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;team_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;team_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;channel_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;channel_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message_ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;message_ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;post_internal_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;internal_payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="nf"&gt;reply_via_response_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;response_url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;approval received, internal action triggered&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;reply_via_response_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;response_url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;action failed, check logs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;threading&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Thread&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;work&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;daemon&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;send_alert_with_button&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.0.0.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PORT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8080&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Ops notes: routing, UX, security, links, and SEO
&lt;/h2&gt;

&lt;h3&gt;
  
  
  When to use Slack vs paging tools vs Discord
&lt;/h3&gt;

&lt;p&gt;This page is about mechanics. Routing is strategy. Still, the boundary is easy to describe.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Channel&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;th&gt;Failure mode&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PagerDuty or equivalent&lt;/td&gt;
&lt;td&gt;Urgent user impact requiring immediate response&lt;/td&gt;
&lt;td&gt;People sleep through Slack&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Slack&lt;/td&gt;
&lt;td&gt;Coordination, approvals, workflow execution&lt;/td&gt;
&lt;td&gt;Noise and channel fatigue&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Discord&lt;/td&gt;
&lt;td&gt;Teams that live in Discord, lighter weight control loops&lt;/td&gt;
&lt;td&gt;Less enterprise workflow structure&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Use Slack when you want the conversation and the workflow to be the interface. Use paging tools when the alert is not optional. If you are balancing Slack interaction design against service boundaries and persistence choices, &lt;a href="https://www.glukhov.org/app-architecture/" rel="noopener noreferrer"&gt;this app architecture overview&lt;/a&gt; helps place that decision in the larger system. For a deeper routing model, see &lt;a href="https://www.glukhov.org/observability/alerting/" rel="noopener noreferrer"&gt;Modern Alerting Systems Design for Observability Teams&lt;/a&gt;. For a Discord integration alternative, see &lt;a href="https://www.glukhov.org/app-architecture/integration-patterns/discord/" rel="noopener noreferrer"&gt;Discord Integration Pattern for Alerts and Control Loops&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Accessibility and UX notes
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Put high volume alerts into their own channel, and keep human discussion in threads.&lt;/li&gt;
&lt;li&gt;Use threads for per-incident context and updates. response_url can post in-channel and in threads when thread_ts is provided (&lt;a href="https://docs.slack.dev/interactivity/handling-user-interaction/" rel="noopener noreferrer"&gt;Handling user interaction&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;Use ephemeral responses when acknowledging user actions to avoid channel spam, but remember ephemeral delivery is not guaranteed and is session dependent (&lt;a href="https://docs.slack.dev/reference/methods/chat.postEphemeral/" rel="noopener noreferrer"&gt;chat.postEphemeral&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;Use Block Kit Builder to prototype layouts quickly (&lt;a href="https://docs.slack.dev/block-kit/" rel="noopener noreferrer"&gt;Block Kit&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;If you add images, include meaningful alt text where supported and keep a plain text fallback in the top level text field.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Security checklist
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Verify every incoming Slack request using signing secret headers and raw body (&lt;a href="https://docs.slack.dev/authentication/verifying-requests-from-slack/" rel="noopener noreferrer"&gt;Verifying requests from Slack&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;Reject requests with timestamps older than five minutes to reduce replay risk (&lt;a href="https://docs.slack.dev/authentication/verifying-requests-from-slack/" rel="noopener noreferrer"&gt;Verifying requests from Slack&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;Keep tokens and webhook URLs in a secret manager, never in git.&lt;/li&gt;
&lt;li&gt;Use least privilege OAuth scopes and rotate secrets when people change roles (&lt;a href="https://docs.slack.dev/reference/scopes/" rel="noopener noreferrer"&gt;Scopes reference&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;Authenticate and authorize the internal action API separately, do not treat Slack as an auth boundary.&lt;/li&gt;
&lt;li&gt;Make approvals idempotent and deduplicated.&lt;/li&gt;
&lt;li&gt;Log approvals in an audit friendly way, including team, channel, message timestamp, user, and action.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Slack is at its best when you treat it as a systems boundary, not a message sink. Incoming webhooks cover fast alert delivery. Apps plus interactivity turn Slack into a workflow engine and event interface. The hard parts are signature verification, timing constraints, deduplication, and choosing where Slack fits in your alert routing model.&lt;/p&gt;

&lt;p&gt;Next links:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.glukhov.org/app-architecture/integration-patterns/chat-platforms-as-system-interfaces/" rel="noopener noreferrer"&gt;Chat Platforms as System Interfaces in Modern Systems&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.glukhov.org/app-architecture/integration-patterns/discord/" rel="noopener noreferrer"&gt;Discord Integration Pattern for Alerts and Control Loops&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.glukhov.org/observability/alerting/" rel="noopener noreferrer"&gt;Modern Alerting Systems Design for Observability Teams&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>integration</category>
      <category>alerting</category>
      <category>observability</category>
      <category>go</category>
    </item>
    <item>
      <title>Discord Integration Pattern for Alerts and Control Loops</title>
      <dc:creator>Rost</dc:creator>
      <pubDate>Wed, 15 Apr 2026 14:16:44 +0000</pubDate>
      <link>https://dev.to/rosgluk/discord-integration-pattern-for-alerts-and-control-loops-3f9h</link>
      <guid>https://dev.to/rosgluk/discord-integration-pattern-for-alerts-and-control-loops-3f9h</guid>
      <description>&lt;p&gt;Discord becomes a serious integration surface when you treat it like one: a place where systems publish events, humans make decisions, and automation continues the workflow.&lt;/p&gt;

&lt;p&gt;This deep dive frames Discord in three modes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Notification sink for one way alerts via incoming webhooks.&lt;/li&gt;
&lt;li&gt;Command surface for explicit actions via application commands and components.&lt;/li&gt;
&lt;li&gt;Event subscription layer where reactions and interactions become triggers via Gateway events.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This page is about shaping the boundary between your systems and a chat UI.&lt;br&gt;
It is not a guide to alert philosophy or paging thresholds.&lt;br&gt;
For alert strategy and routing, see &lt;a href="https://www.glukhov.org/observability/alerting/" rel="noopener noreferrer"&gt;Modern Alerting Systems Design for Observability Teams&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Discord in app architecture - integration patterns
&lt;/h2&gt;

&lt;p&gt;Discord is not an observability product and it is not a developer tool. It is an integration endpoint with a distinctive property: the user interface is a shared conversation that can also act as an event source.&lt;/p&gt;

&lt;p&gt;In Discord, a system can post an event and a human can respond with an approval signal. Your system can then subscribe to that signal via Gateway events. That boundary is an integration-patterns problem.&lt;/p&gt;

&lt;p&gt;Incoming webhooks make Discord a low effort way to post messages to channels without running a bot session or managing a persistent connection. This is why webhooks are a pragmatic default for one way alerts. When you need bidirectional control, the shape changes to a bot over the Gateway or an interactions endpoint. See &lt;a href="https://docs.discord.com/developers/platform/webhooks" rel="noopener noreferrer"&gt;Discord webhooks&lt;/a&gt; and the &lt;a href="https://docs.discord.com/developers/resources/webhook" rel="noopener noreferrer"&gt;Webhook resource reference&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For the broader framing across Slack and Discord, see &lt;a href="https://www.glukhov.org/app-architecture/integration-patterns/chat-platforms-as-system-interfaces/" rel="noopener noreferrer"&gt;Chat Platforms as System Interfaces in Modern Systems&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Discord as a system interface
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Discord as a notification sink
&lt;/h3&gt;

&lt;p&gt;A notification sink is a one way integration: your service emits a message and the channel displays it.&lt;/p&gt;

&lt;p&gt;Incoming webhooks are designed for this. They are HTTP endpoints tied to a channel, and a POST creates a message without requiring a bot user or a persistent gateway connection. See &lt;a href="https://docs.discord.com/developers/platform/webhooks" rel="noopener noreferrer"&gt;Incoming webhooks&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This mode fits status updates, build notifications, and operational signals where the desired action is simply "be aware".&lt;/p&gt;
&lt;h3&gt;
  
  
  Discord as a command surface
&lt;/h3&gt;

&lt;p&gt;A command surface is where humans explicitly ask the system to do something.&lt;/p&gt;

&lt;p&gt;In Discord, this is most cleanly implemented with application commands, message components, and interaction responses. See &lt;a href="https://docs.discord.com/developers/interactions/application-commands" rel="noopener noreferrer"&gt;Application commands&lt;/a&gt; and &lt;a href="https://docs.discord.com/developers/components/reference" rel="noopener noreferrer"&gt;Components reference&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This mode also supports ephemeral messages (visible only to the invoking user) for acknowledgements and low value confirmations, because interactions support an ephemeral flag. See &lt;a href="https://docs.discord.com/developers/interactions/receiving-and-responding" rel="noopener noreferrer"&gt;Receiving and responding to interactions&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  Discord as an event subscription layer
&lt;/h3&gt;

&lt;p&gt;An event subscription layer is where humans do not issue a command. They react to a message and the system treats it as a signal. The classic example is "react with a thumbs up to approve".&lt;/p&gt;

&lt;p&gt;Technically, you receive these via Gateway events such as Message Reaction Add, which requires selecting the right gateway intents during identify. See &lt;a href="https://docs.discord.com/developers/events/gateway" rel="noopener noreferrer"&gt;Gateway docs&lt;/a&gt; and the &lt;a href="https://docs.discord.com/developers/events/gateway-events" rel="noopener noreferrer"&gt;Gateway Events reference&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Opinionated take: reactions are best when the decision is simple and the action is low friction. Once a workflow needs parameters, state, or multiple outcomes, reactions start to feel like a hack. Buttons and commands age better.&lt;/p&gt;
&lt;h2&gt;
  
  
  Architecture patterns
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Pattern one simple webhook flow
&lt;/h3&gt;

&lt;p&gt;This is the simplest production shape: your system routes an alert to a Discord webhook and stops there.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[service] -&amp;gt; [alert router] -&amp;gt; [discord webhook] -&amp;gt; [channel]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A practical detail that matters: Discord has message and embed limits. The Message Create docs list content up to 2000 characters, and embeds have their own limits including up to 10 embeds and an overall embed size limit. See &lt;a href="https://docs.discord.com/developers/resources/message" rel="noopener noreferrer"&gt;Message resource&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pattern two brokered flow with a message queue
&lt;/h3&gt;

&lt;p&gt;Once chat delivery becomes critical, many teams avoid having production services talk to Discord directly. A broker absorbs spikes and gives you a place to retry and deduplicate.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[service] -&amp;gt; [queue topic] -&amp;gt; [alert dispatcher] -&amp;gt; [discord]
                                 |
                                 +-&amp;gt; [dead letter queue]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Discord documents per route and global rate limits and returns rate limit headers plus HTTP 429. See &lt;a href="https://docs.discord.com/developers/topics/rate-limits" rel="noopener noreferrer"&gt;Discord rate limits&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This pattern is why "fastest way to send alerts to Discord" is often webhooks, but "most robust way" is usually a dispatcher sitting behind a queue.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pattern three control loop pattern
&lt;/h3&gt;

&lt;p&gt;This is the human in the loop control loop: an alert is posted, a small set of users approve, and the system executes an action.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[alert] -&amp;gt; [discord message] -&amp;gt; [human reaction] -&amp;gt; [bot] -&amp;gt; [internal action API]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pattern is the reason Discord belongs under integration patterns: the integration is not only notification, it is decision and control.&lt;/p&gt;

&lt;h3&gt;
  
  
  Alert and approval workflow diagram
&lt;/h3&gt;

&lt;h2&gt;
  
  
  Webhook versus bot
&lt;/h2&gt;

&lt;p&gt;Webhooks are strong for one way delivery. Bots are required when you need to read events (reactions, commands, and components) in near real time.&lt;/p&gt;

&lt;p&gt;A pragmatic comparison:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Capability&lt;/th&gt;
&lt;th&gt;Webhook&lt;/th&gt;
&lt;th&gt;Bot over Gateway&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Post messages&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Receive reactions&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Receive commands or buttons&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Persistent connection&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Secret management&lt;/td&gt;
&lt;td&gt;Webhook URL&lt;/td&gt;
&lt;td&gt;Bot token plus permissions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best fit&lt;/td&gt;
&lt;td&gt;Alerts and notifications&lt;/td&gt;
&lt;td&gt;Approvals, control loops, workflows&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Webhooks do not require a bot user or authentication beyond the unguessable webhook URL, while Gateway event reception depends on identify plus intents. See &lt;a href="https://docs.discord.com/developers/resources/webhook" rel="noopener noreferrer"&gt;Webhook resource&lt;/a&gt; and &lt;a href="https://docs.discord.com/developers/events/gateway" rel="noopener noreferrer"&gt;Gateway receiving events and intents&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recommended libraries for Go and Python
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Go
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;discordgo is the long running Go binding for Discord, with event handlers and REST methods. See the &lt;a href="https://github.com/bwmarrin/discordgo" rel="noopener noreferrer"&gt;discordgo repo&lt;/a&gt; and its API docs on &lt;a href="https://pkg.go.dev/github.com/bwmarrin/discordgo" rel="noopener noreferrer"&gt;pkg.go.dev&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Python
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;discord.py is the canonical async wrapper. See the &lt;a href="https://github.com/Rapptz/discord.py" rel="noopener noreferrer"&gt;Rapptz discord.py repo&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;nextcord is a maintained fork with its own docs. See the &lt;a href="https://github.com/nextcord/nextcord" rel="noopener noreferrer"&gt;nextcord repo&lt;/a&gt; and &lt;a href="https://docs.nextcord.dev/en/stable/index.html" rel="noopener noreferrer"&gt;nextcord docs&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Opinionated take: for operational integrations, a Go service built on discordgo is often easy to package and deploy as a single binary. Python shines for quick iteration and glue logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Message design for alerts in Discord
&lt;/h2&gt;

&lt;h3&gt;
  
  
  A compact alert template
&lt;/h3&gt;

&lt;p&gt;To keep alerts actionable, a stable message schema helps.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;title&lt;/td&gt;
&lt;td&gt;The issue in one line&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;severity&lt;/td&gt;
&lt;td&gt;info, warn, critical&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;context&lt;/td&gt;
&lt;td&gt;Identifiers and links needed to decide&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;action_hint&lt;/td&gt;
&lt;td&gt;The next action, including the approval signal&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Example values:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;title: "checkout error rate elevated"&lt;/li&gt;
&lt;li&gt;severity: "warn"&lt;/li&gt;
&lt;li&gt;context: "service=checkout env=prod region=us-east"&lt;/li&gt;
&lt;li&gt;action_hint: "react with custom emoji thumbsup to trigger restart"&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Webhook payload example
&lt;/h3&gt;

&lt;p&gt;Incoming webhooks accept JSON and can post content, embeds, or both. See &lt;a href="https://docs.discord.com/developers/platform/webhooks" rel="noopener noreferrer"&gt;Incoming webhooks docs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This example uses embeds for structure and disables automatic mention parsing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"username"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"alert-router"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"embeds"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"checkout error rate elevated"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"single message, structured fields"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"fields"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"severity"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"warn"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"inline"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"context"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"service=checkout env=prod region=us-east"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"inline"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"action_hint"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"react with custom emoji thumbsup to trigger restart"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"inline"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"allowed_mentions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"parse"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Discord documents allowed_mentions and why it matters for avoiding "phantom pings". See &lt;a href="https://docs.discord.com/developers/resources/message" rel="noopener noreferrer"&gt;Allowed mentions in Message resource&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementation deep dive for reaction driven approvals
&lt;/h2&gt;

&lt;p&gt;The FAQ questions about capturing reactions, avoiding missed approvals, and triggering actions safely reduce to four areas: intents, matching, idempotency, and security.&lt;/p&gt;

&lt;h3&gt;
  
  
  Gateway intents and privileged intents
&lt;/h3&gt;

&lt;p&gt;Reaction events are delivered over the Gateway and depend on specifying intents during identify. See &lt;a href="https://docs.discord.com/developers/events/gateway" rel="noopener noreferrer"&gt;Gateway receiving events and intents&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If an integration also needs role based allowlists, it may drift toward member state and member caching, which can involve enabling the privileged Server Members intent in the Developer Portal. Discord documents privileged intents and access requirements for larger scale apps. See &lt;a href="https://support-dev.discord.com/hc/en-us/articles/6207308062871-What-are-Privileged-Intents" rel="noopener noreferrer"&gt;What are privileged intents&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reaction matching and custom emoji
&lt;/h3&gt;

&lt;p&gt;If you use the standard thumbs up emoji, the emoji name is a unicode glyph. To keep matching stable and ASCII friendly, some teams add a custom guild emoji named thumbsup and match on that.&lt;/p&gt;

&lt;p&gt;Discord documents custom emoji encoding as name:id for reaction endpoints. See the &lt;a href="https://docs.discord.com/developers/resources/message" rel="noopener noreferrer"&gt;Create Reaction section in Message resource&lt;/a&gt;. discordgo also states that reactions use either a unicode emoji or a guild emoji identifier in name:id format. See &lt;a href="https://pkg.go.dev/github.com/bwmarrin/discordgo#Session.MessageReactionAdd" rel="noopener noreferrer"&gt;discordgo Session.MessageReactionAdd docs&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Idempotency and deduplication
&lt;/h3&gt;

&lt;p&gt;Treat reaction approvals as at least once events. Duplicate deliveries can happen after reconnects, retries, or library internal behavior.&lt;/p&gt;

&lt;p&gt;A practical idempotency key for a reaction driven approve is:&lt;/p&gt;

&lt;p&gt;message_id + user_id + emoji + action&lt;/p&gt;

&lt;p&gt;Brokered flows often store this key in Redis with a TTL that matches the workflow window.&lt;/p&gt;

&lt;p&gt;Discord also supports a nonce on message creation, and can enforce nonce uniqueness for a short window. See nonce and enforce_nonce in the &lt;a href="https://docs.discord.com/developers/resources/message" rel="noopener noreferrer"&gt;Message Create params&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rate limits and backoff
&lt;/h3&gt;

&lt;p&gt;Discord rate limits apply to both bots and webhooks. In HTTP 429 responses, Discord returns rate limit related headers and a Retry After value. See &lt;a href="https://docs.discord.com/developers/topics/rate-limits" rel="noopener noreferrer"&gt;Rate limits&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In practice, heavy alerting pushes teams toward:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;grouping and batching&lt;/li&gt;
&lt;li&gt;per channel throttling&lt;/li&gt;
&lt;li&gt;exponential backoff with jitter&lt;/li&gt;
&lt;li&gt;a dead letter queue for poison payloads&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Go example send alert and approve with reaction
&lt;/h2&gt;

&lt;p&gt;Prereqs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create a bot in the Discord Developer Portal and invite it to your server using OAuth2. See &lt;a href="https://docs.discord.com/developers/platform/oauth2-and-permissions" rel="noopener noreferrer"&gt;OAuth2 and permissions&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Give the bot permissions to read the channel, send messages, and read message history.&lt;/li&gt;
&lt;li&gt;Configure gateway intents to receive guild message reactions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Note: this example matches a custom guild emoji named thumbsup. That represents the "thumbs up" approval signal without embedding a unicode emoji in code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;package&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s"&gt;"bytes"&lt;/span&gt;
  &lt;span class="s"&gt;"encoding/json"&lt;/span&gt;
  &lt;span class="s"&gt;"log"&lt;/span&gt;
  &lt;span class="s"&gt;"net/http"&lt;/span&gt;
  &lt;span class="s"&gt;"os"&lt;/span&gt;
  &lt;span class="s"&gt;"strings"&lt;/span&gt;
  &lt;span class="s"&gt;"sync"&lt;/span&gt;
  &lt;span class="s"&gt;"time"&lt;/span&gt;

  &lt;span class="s"&gt;"github.com/bwmarrin/discordgo"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;ActionRequest&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;AlertID&lt;/span&gt;   &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`json:"alert_id"`&lt;/span&gt;
  &lt;span class="n"&gt;MessageID&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`json:"message_id"`&lt;/span&gt;
  &lt;span class="n"&gt;UserID&lt;/span&gt;    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`json:"user_id"`&lt;/span&gt;
  &lt;span class="n"&gt;Action&lt;/span&gt;    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`json:"action"`&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;targetMessageID&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;

  &lt;span class="n"&gt;seenMu&lt;/span&gt; &lt;span class="n"&gt;sync&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Mutex&lt;/span&gt;
  &lt;span class="n"&gt;seen&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt;
  &lt;span class="n"&gt;ttl&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Minute&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"DISCORD_BOT_TOKEN"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;channelID&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"DISCORD_CHANNEL_ID"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;internalURL&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"INTERNAL_API_URL"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;thumbsEmoji&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"THUMBSUP_EMOJI"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// custom guild emoji name:id, e.g. thumbsup:123456789012345678&lt;/span&gt;
  &lt;span class="n"&gt;approverUsers&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;splitCSV&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"APPROVER_USER_IDS"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="c"&gt;// comma separated snowflake IDs&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;channelID&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;internalURL&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fatal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Missing env vars DISCORD_BOT_TOKEN DISCORD_CHANNEL_ID INTERNAL_API_URL"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="n"&gt;dg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;discordgo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Bot "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fatalf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"discordgo.New failed: %v"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c"&gt;// Receive reaction events. Keep intents tight.&lt;/span&gt;
  &lt;span class="n"&gt;dg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Identify&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Intents&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;discordgo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IntentsGuildMessages&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;discordgo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IntentsGuildMessageReactions&lt;/span&gt;

  &lt;span class="n"&gt;dg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddHandlerOnce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;discordgo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Session&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;discordgo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Ready&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ChannelMessageSend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;channelID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;alertText&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"send alert failed: %v"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;targetMessageID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;
    &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"posted alert message_id=%s"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;targetMessageID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c"&gt;// Optional convenience: pre-add the approval reaction so users can click it.&lt;/span&gt;
    &lt;span class="c"&gt;// For custom emojis, Discord expects name:id. For unicode emojis, it is the glyph.&lt;/span&gt;
    &lt;span class="c"&gt;// See Message Create and Create Reaction in Discord Message resource.&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;thumbsEmoji&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MessageReactionAdd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;channelID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;targetMessageID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;thumbsEmoji&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="n"&gt;dg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;discordgo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Session&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ev&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;discordgo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MessageReactionAdd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ev&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MessageReaction&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c"&gt;// Only handle reactions for the message we just posted.&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;targetMessageID&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MessageID&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;targetMessageID&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c"&gt;// Ignore bot's own reactions.&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;State&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UserID&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c"&gt;// Match custom emoji name. If you use the standard emoji, Emoji.Name will be a unicode glyph.&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Emoji&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s"&gt;"thumbsup"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c"&gt;// Allowlist. Role based checks often pull in member state and sometimes privileged intents.&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;isAllowlisted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UserID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;approverUsers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"deny approval user_id=%s"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UserID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c"&gt;// Dedupe approvals. In production, store this in Redis.&lt;/span&gt;
    &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MessageID&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;":"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UserID&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;":"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Emoji&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;":approve"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;tryOnce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;req&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;ActionRequest&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;AlertID&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;   &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ALERT_ID"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="n"&gt;MessageID&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MessageID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;UserID&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UserID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;Action&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;    &lt;span class="s"&gt;"approve_restart"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;postJSON&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;internalURL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"action POST failed: %v"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ChannelMessageSend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;channelID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"approval received, action triggered"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;dg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Open&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fatalf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"dg.Open failed: %v"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;dg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"discord bot running"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;alertText&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;"[warn] checkout error rate elevated&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
    &lt;span class="s"&gt;"context service=checkout env=prod&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
    &lt;span class="s"&gt;"action_hint react with custom emoji thumbsup to trigger restart"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;splitCSV&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TrimSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="n"&gt;parts&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;","&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;parts&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TrimSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;isAllowlisted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userID&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;allow&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;allow&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;allow&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;userID&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;tryOnce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="n"&gt;seenMu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Lock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;seenMu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Unlock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="c"&gt;// Lazy cleanup.&lt;/span&gt;
  &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;seen&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ttl&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nb"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="n"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;postJSON&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Marshal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MethodPost&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewReader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Header&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Content-Type"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"application/json"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Timeout&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Do&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusCode&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="m"&gt;200&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusCode&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="m"&gt;300&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;httpError&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusCode&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;httpError&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;httpError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;"http status "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Python example send alert and approve with reaction
&lt;/h2&gt;

&lt;p&gt;This example uses discord.py style events. A key reliability detail is that cache dependent reaction events can silently fail if the message is not in cache. The discord.py community commonly points to raw reaction events for this reason. See &lt;a href="https://github.com/Rapptz/discord.py/discussions/8369" rel="noopener noreferrer"&gt;discord.py discussions on raw reaction events&lt;/a&gt; and &lt;a href="https://deepwiki.com/Rapptz/discord.py/3.8-raw-event-models" rel="noopener noreferrer"&gt;raw event models&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Note: this example matches a custom guild emoji named thumbsup, representing the "thumbs up" approval signal without embedding a unicode emoji literal in code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;aiohttp&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;discord&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Dict&lt;/span&gt;

&lt;span class="n"&gt;DISCORD_BOT_TOKEN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DISCORD_BOT_TOKEN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;DISCORD_CHANNEL_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DISCORD_CHANNEL_ID&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="n"&gt;INTERNAL_API_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INTERNAL_API_URL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="c1"&gt;# Comma separated snowflake IDs of approvers
&lt;/span&gt;&lt;span class="n"&gt;APPROVER_USER_IDS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;APPROVER_USER_IDS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# In production, persist this in Redis or a database
&lt;/span&gt;&lt;span class="n"&gt;_seen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="n"&gt;_TTL_SECONDS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;600.0&lt;/span&gt;

&lt;span class="n"&gt;intents&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;discord&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Intents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;default&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;intents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;guilds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
&lt;span class="n"&gt;intents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
&lt;span class="n"&gt;intents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reactions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;discord&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;intents&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;intents&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;target_message_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_try_once&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_event_loop&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;expired&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;_seen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="nf"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_TTL_SECONDS&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;expired&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;_seen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;_seen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
    &lt;span class="n"&gt;_seen&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_post_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;alert_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;alert_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;alert_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message_id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;action&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;approve_restart&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;aiohttp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ClientSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;aiohttp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ClientTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;INTERNAL_API_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;internal api http &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@client.event&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;on_ready&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;global&lt;/span&gt; &lt;span class="n"&gt;target_message_id&lt;/span&gt;

    &lt;span class="n"&gt;ch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_channel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DISCORD_CHANNEL_ID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ch&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;channel not found or missing permissions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[warn] checkout error rate elevated&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s"&gt;n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;context service=checkout env=prod&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s"&gt;n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;action_hint react with custom emoji thumbsup to trigger restart&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;target_message_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;

    &lt;span class="c1"&gt;# Optional convenience: pre-add a custom emoji named thumbsup (server emoji).
&lt;/span&gt;    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;emojis&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;thumbsup&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_reaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;discord&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;pass&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;

    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ready posted message_id=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;target_message_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@client.event&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;on_raw_reaction_add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;discord&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RawReactionActionEvent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;global&lt;/span&gt; &lt;span class="n"&gt;target_message_id&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;target_message_id&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;message_id&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;target_message_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;

    &lt;span class="c1"&gt;# Ignore the bot account itself
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;

    &lt;span class="c1"&gt;# Allowlist
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;APPROVER_USER_IDS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;

    &lt;span class="c1"&gt;# Match custom emoji name
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;emoji&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;thumbsup&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;

    &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;message_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;emoji&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;:approve&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;_try_once&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;

    &lt;span class="n"&gt;alert_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ALERT_ID&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;_post_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;alert_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;message_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;action failed &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;

    &lt;span class="n"&gt;ch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_channel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;channel_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ch&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;approval received, action triggered&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DISCORD_BOT_TOKEN&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Interaction patterns that scale beyond demos
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Reaction driven workflows
&lt;/h3&gt;

&lt;p&gt;Reaction approvals are cheap. They also hide complexity:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;reactions are ambiguous without context&lt;/li&gt;
&lt;li&gt;duplicates happen&lt;/li&gt;
&lt;li&gt;you need an allowlist&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If reactions remain the UI, a few patterns tend to help:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;store the target message ID (and optionally a related alert ID)&lt;/li&gt;
&lt;li&gt;store an idempotency key&lt;/li&gt;
&lt;li&gt;log who approved and when&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Role based actions
&lt;/h3&gt;

&lt;p&gt;Role checks match how teams think, but they tend to pull in member state. Operationally, this can push you toward privileged intents and member caching.&lt;/p&gt;

&lt;p&gt;A compromise that often ages well:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;start with an explicit allowlist of approver user IDs&lt;/li&gt;
&lt;li&gt;later, add role checks once the role model and permissions are stable&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Multi step flows
&lt;/h3&gt;

&lt;p&gt;Multi step flows are where reactions start to crack. If the bot needs to ask a question or present options, components and commands are usually a better fit.&lt;/p&gt;

&lt;p&gt;Discord supports components for richer interactive messages. See the &lt;a href="https://docs.discord.com/developers/components/reference" rel="noopener noreferrer"&gt;Components reference&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Safety strategies
&lt;/h3&gt;

&lt;p&gt;A control loop that can restart production needs guardrails. Common guardrails include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;require two approvals&lt;/li&gt;
&lt;li&gt;require approvals within a time window&lt;/li&gt;
&lt;li&gt;require the alert to still be active&lt;/li&gt;
&lt;li&gt;require the internal action endpoint to be idempotent&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Observability routing Discord versus PagerDuty versus Slack
&lt;/h2&gt;

&lt;p&gt;The FAQ question about when Discord should be used instead of a paging tool is fundamentally a routing strategy question.&lt;/p&gt;

&lt;p&gt;The SRE view is that paging should interrupt a human only for issues that need immediate action, and alerts should be actionable and based on symptoms. See &lt;a href="https://sre.google/sre-book/monitoring-distributed-systems/" rel="noopener noreferrer"&gt;Google SRE Monitoring Distributed Systems&lt;/a&gt; and the &lt;a href="https://static.googleusercontent.com/media/sre.google/en//static/pdf/IncidentManagementGuide.pdf" rel="noopener noreferrer"&gt;Google SRE Incident Management Guide PDF&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;A practical split that tends to reduce noise:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PagerDuty or equivalent for urgent user impact where someone must wake up&lt;/li&gt;
&lt;li&gt;Slack for coordinated incident operations and structured workflows in many organizations&lt;/li&gt;
&lt;li&gt;Discord for teams that live in Discord, and for lightweight approvals and control signals&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This page focuses on integration mechanics. If you are deciding how Discord approvals should sit alongside service design and data boundaries, &lt;a href="https://www.glukhov.org/app-architecture/" rel="noopener noreferrer"&gt;this app architecture overview&lt;/a&gt; gives the wider context for those trade-offs. For strategy, severity models, and channel selection, see &lt;a href="https://www.glukhov.org/observability/alerting/" rel="noopener noreferrer"&gt;Modern Alerting Systems Design for Observability Teams&lt;/a&gt;. For a Slack alternative, see &lt;a href="https://www.glukhov.org/app-architecture/integration-patterns/slack/" rel="noopener noreferrer"&gt;Slack Integration Patterns for Alerts and Workflows&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reliability notes that matter in production
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Cache behavior and raw reaction events
&lt;/h3&gt;

&lt;p&gt;Cache dependent reaction events are a common source of flakiness in chat ops bots. Raw reaction events exist specifically to avoid dependency on message cache state. See &lt;a href="https://github.com/Rapptz/discord.py/discussions/8369" rel="noopener noreferrer"&gt;discord.py discussions&lt;/a&gt; and &lt;a href="https://deepwiki.com/Rapptz/discord.py/3.8-raw-event-models" rel="noopener noreferrer"&gt;raw event models&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Retries and at least once delivery
&lt;/h3&gt;

&lt;p&gt;Assume at least once delivery. If your bot retries an internal API call, duplicates can be created unless the internal API is idempotent.&lt;/p&gt;

&lt;p&gt;A pragmatic design is to accept an idempotency key on the internal API and enforce uniqueness there, not only in the bot.&lt;/p&gt;

&lt;h3&gt;
  
  
  Backpressure
&lt;/h3&gt;

&lt;p&gt;If Discord is rate limited, queues help. Discord describes rate limit buckets, global limits, and headers. See &lt;a href="https://docs.discord.com/developers/topics/rate-limits" rel="noopener noreferrer"&gt;Rate limits&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security deep dive
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Tokens, scopes, and permissions
&lt;/h3&gt;

&lt;p&gt;For bots, a bot token authenticates the session. For installation, Discord uses OAuth2 scopes and permission bitfields. See &lt;a href="https://docs.discord.com/developers/platform/oauth2-and-permissions" rel="noopener noreferrer"&gt;OAuth2 and permissions&lt;/a&gt; and &lt;a href="https://docs.discord.com/developers/topics/oauth2" rel="noopener noreferrer"&gt;OAuth2 topics&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;A bot that can manage messages or manage roles is a production risk. Least privilege is less about ideology and more about reducing the blast radius of a leaked token.&lt;/p&gt;

&lt;h3&gt;
  
  
  Verifying signed requests for interactions
&lt;/h3&gt;

&lt;p&gt;If you build an interactions endpoint (slash commands and components delivered over HTTP), Discord requires validating request headers including X-Signature-Ed25519 and X-Signature-Timestamp. See &lt;a href="https://docs.discord.com/developers/interactions/overview" rel="noopener noreferrer"&gt;Interactions overview&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Snowflake IDs and auditability
&lt;/h3&gt;

&lt;p&gt;Discord IDs are snowflakes and are returned as strings in the HTTP API due to size. Storing user IDs, message IDs, and channel IDs as strings in logs is normal. See &lt;a href="https://docs.discord.com/developers/reference" rel="noopener noreferrer"&gt;Discord API Reference Snowflakes&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Security checklist
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Store bot tokens and webhook URLs in a secret manager, never in git.&lt;/li&gt;
&lt;li&gt;Use least privilege permissions for the bot role.&lt;/li&gt;
&lt;li&gt;In the internal action API, require authentication and validate the caller identity.&lt;/li&gt;
&lt;li&gt;Allowlist approvers by user ID and optionally role.&lt;/li&gt;
&lt;li&gt;Make internal actions idempotent and deduplicate reaction events.&lt;/li&gt;
&lt;li&gt;Log approvals with message ID, user ID, action, and timestamp.&lt;/li&gt;
&lt;li&gt;If using interactions over HTTP, verify Discord signatures.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Accessibility and UX notes
&lt;/h2&gt;

&lt;p&gt;Discord is a UI. Treat it like one.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use threads for each alert to keep channels readable.&lt;/li&gt;
&lt;li&gt;Use channel naming and separation by severity so high signal alerts do not drown in chatter.&lt;/li&gt;
&lt;li&gt;Prefer short messages with structured embeds rather than walls of text.&lt;/li&gt;
&lt;li&gt;When using commands and components, ephemeral responses can reduce channel noise. Ephemeral behavior is documented for interactions. See &lt;a href="https://docs.discord.com/developers/interactions/receiving-and-responding" rel="noopener noreferrer"&gt;Receiving and responding to interactions&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Discord is unusually useful when you stop thinking of it as chat and start treating it as a system interface. Webhooks cover the notification sink. Bots and gateway events cover approvals and control loops. The hard parts are not syntax. They are routing, idempotency, and security.&lt;/p&gt;

&lt;p&gt;For the broader framing, jump to &lt;a href="https://www.glukhov.org/app-architecture/integration-patterns/chat-platforms-as-system-interfaces/" rel="noopener noreferrer"&gt;Chat Platforms as System Interfaces in Modern Systems&lt;/a&gt;. For alert strategy, see &lt;a href="https://www.glukhov.org/observability/alerting/" rel="noopener noreferrer"&gt;Modern Alerting Systems Design for Observability Teams&lt;/a&gt;. For a Slack based alternative, compare approaches at &lt;a href="https://www.glukhov.org/app-architecture/integration-patterns/slack/" rel="noopener noreferrer"&gt;Slack Integration Patterns for Alerts and Workflows&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>integration</category>
      <category>observability</category>
      <category>alerting</category>
      <category>bots</category>
    </item>
  </channel>
</rss>
