<?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: Doug Sillars</title>
    <description>The latest articles on DEV Community by Doug Sillars (@dougsillars).</description>
    <link>https://dev.to/dougsillars</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F100376%2F5694c39e-09c7-4eb4-8fd2-4924967b2144.png</url>
      <title>DEV Community: Doug Sillars</title>
      <link>https://dev.to/dougsillars</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dougsillars"/>
    <language>en</language>
    <item>
      <title>The Replay Bundle That Remembers What Happened</title>
      <dc:creator>Doug Sillars</dc:creator>
      <pubDate>Tue, 25 Aug 2026 12:58:42 +0000</pubDate>
      <link>https://dev.to/cognous/the-replay-bundle-that-remembers-what-happened-48l1</link>
      <guid>https://dev.to/cognous/the-replay-bundle-that-remembers-what-happened-48l1</guid>
      <description>&lt;p&gt;It's 2am. A customer complains about something an agent did. The next morning the CEO is already asking the only question that matters: did the agent do something it shouldn't have?&lt;/p&gt;

&lt;p&gt;"I'm pretty sure our guardrails are solid" isn't an answer anyone wants to give the boss. It certainly won't fly with a customer, an auditor, or with legal. What they want is proof, and proof means being able to answer specifically: what did the agent try to do, on this exact run, and what happened when it tried?&lt;/p&gt;

&lt;p&gt;If that answer lives in the timestamps of a few hundred servers and a half-dozen tool integrations, you don't have proof. You have a scavenger hunt. "Trust us, it's fine" does not survive a scavenger hunt.&lt;/p&gt;

&lt;p&gt;Cognous' Open Control Stack exists to close that gap. It follows four steps: Declare → Control → Replay → Evidence.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Declare:&lt;/strong&gt; Agents need guardrails to tell them what they can and cannot do — that's a &lt;a href="https://dev.to/cognous/the-manifest-that-keeps-your-ai-agent-honest-3e97"&gt;manifest&lt;/a&gt;, declared up front.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Control:&lt;/strong&gt; They need something enforcing those guardrails live — that's a &lt;a href="https://dev.to/cognous/cognous-control-plane-the-layer-that-tells-your-agent-no-3ln9"&gt;control plane&lt;/a&gt;, deciding allow/block/escalate on every proposal in real time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this post, we'll cover Replay: no longer defining and patrolling the guardrails, but the receipts that the rails held. Taking what the Control Plane recorded during a run and turning it into one portable file — the thing you actually hand someone instead of a text file with pasted logs from a dozen systems.&lt;/p&gt;

&lt;h2&gt;
  
  
  What The Control Plane Hands Off
&lt;/h2&gt;

&lt;p&gt;Picture a workflow mid-run: actions getting proposed, the gate deciding each one, live.&lt;/p&gt;

&lt;p&gt;Now what? The Control Plane made the right calls in the moment — but how do you actually know that, after the fact, without having watched it happen live? That's the question Replay answers: it takes what already happened and turns it into something you can go back and inspect.&lt;/p&gt;

&lt;p&gt;The Control Plane keeps a recorder running for the life of a run — that's what's actually holding the frame, the proposals, the decisions, the traces as they happen. At the end of the run, the recorder exports everything it captured as a replay bundle: one file you can store, ship, or hand off for later analysis.&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="n"&gt;bundle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;recorder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate_replay_bundle&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;run_bundle.json&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;w&lt;/span&gt;&lt;span class="sh"&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;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bundle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;model_dump_json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;indent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;run_bundle.json&lt;/code&gt; is the full playback of the workflow — every decision, every trace, and the evidence behind the one blocked action, &lt;code&gt;send_email&lt;/code&gt;, all in one place. It's exactly what you'd look at to understand an error, or hand over if there's a deeper investigation.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Replay Tool
&lt;/h3&gt;

&lt;p&gt;Agent Replay Bundle ships its own CLI, &lt;code&gt;arb&lt;/code&gt;, for working with files like this one — validating, summarizing, redacting, signing. Start with validation, pointed at the bundle the Control Plane just exported:&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="nv"&gt;$ &lt;/span&gt;arb validate run_bundle.json
Error: Replay bundle failed model validation: 1 validation error &lt;span class="k"&gt;for &lt;/span&gt;AgentReplayBundle
bundle_id
  Field required &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;missing]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It fails immediately. The Control Plane's export calls the field &lt;code&gt;replay_bundle_id&lt;/code&gt;; &lt;code&gt;arb&lt;/code&gt;'s schema — the &lt;code&gt;AgentReplayBundle&lt;/code&gt; model that &lt;code&gt;validate&lt;/code&gt; checks against — calls it &lt;code&gt;bundle_id&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The Agent Replay Bundle is a public interchange format, meant for any system, and Agent Control Plane's internal run record shapes several of the schema terms differently than the Replay Bundle. Running validation means performing a mapping step between the two first — small, mostly renames:&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="n"&gt;mapped&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;bundle_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;run&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;replay_bundle_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;run_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;run&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;run_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;generated_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;generated_at&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;frame&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;frame&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_proposals&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;run&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;policy_decisions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;decisions&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;policy_traces&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;policy_traces&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;authority_records&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;authority_records&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;reliance_records&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reliance_records&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;blocked_actions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;blocked_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;final_output&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;final_output&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run that, and the same run record now validates:&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="nv"&gt;$ &lt;/span&gt;arb validate mapped_replay_bundle.json
VALID  &lt;span class="nv"&gt;bundle_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ae564206-8daf-4b19-9d60-db54f56387a5  &lt;span class="nv"&gt;issues&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2
  WARNING W005: Bundle has no authority records.
  WARNING W012: signature_metadata is missing.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Valid — with two warnings, not zero. Neither one blocks validity, and both are honest about the bundle's actual state: no authority was granted for this run (which is &lt;em&gt;why&lt;/em&gt; two of the three actions didn't just allow), and nothing's been signed yet.&lt;/p&gt;

&lt;p&gt;Worth building into your own pipeline: none of this runs automatically. &lt;code&gt;generate_replay_bundle()&lt;/code&gt; builds the object; &lt;code&gt;arb validate&lt;/code&gt; is a separate command a human has to think to run. Nothing stops a bundle from sitting unvalidated for months until the day someone actually needs it — which is the worst possible time to find out it doesn't pass. Wire the mapping and the validation into the end of every run, not into the moment an auditor asks for one.&lt;/p&gt;

&lt;h2&gt;
  
  
  What A Reviewer Sees First
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;arb summarize&lt;/code&gt; is the one-screen version — what you'd look at before deciding whether to open the full bundle:&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="nv"&gt;$ &lt;/span&gt;arb summarize mapped_replay_bundle.json
bundle_id:          ae564206-8daf-4b19-9d60-db54f56387a5
run_id:             c3f474dd-952b-4f27-ab5c-98de868b3c67
status:             &lt;span class="nb"&gt;complete
&lt;/span&gt;actor:              support-agent-v1
environment:        production
policy_version:     v1.0
action_proposals:   3
policy_decisions:   3
policy_traces:      3
blocked_actions:    1
authority_records:  0
reliance_records:   1
final_output:       present
redacted:           no
signed:             no
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three proposals, three decisions, one block, zero authority records. That last number is the tell — it's the same fact the warning surfaced, now visible at a glance instead of buried in a rule name.&lt;/p&gt;

&lt;h2&gt;
  
  
  Redacting For Export
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;mapped_replay_bundle.json&lt;/code&gt; — the bundle we just validated — still has raw targets and payloads in it: customer IDs, draft email bodies, potentially PII. Fine for an internal review, not fine to hand to outside counsel.&lt;/p&gt;

&lt;p&gt;Redaction isn't content-aware — it doesn't scan text and detect what looks sensitive. It replaces a fixed set of fields wholesale: &lt;code&gt;payload&lt;/code&gt; (redacted by default), plus &lt;code&gt;target&lt;/code&gt; and &lt;code&gt;final_output&lt;/code&gt; if you opt in with flags. Everything structural — IDs, timestamps, decision results, policy names — is preserved no matter what:&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="nv"&gt;$ &lt;/span&gt;arb redact mapped_replay_bundle.json &lt;span class="nt"&gt;--out&lt;/span&gt; redacted_bundle.json &lt;span class="nt"&gt;--targets&lt;/span&gt; &lt;span class="nt"&gt;--final-output&lt;/span&gt;
Redacted bundle written to redacted_bundle.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"redaction_metadata"&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;"redacted"&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="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"redacted_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-08-18T19:47:34Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"redacted_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="s2"&gt;"action_proposals[0].payload"&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_proposals[0].target"&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_proposals[1].payload"&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_proposals[1].target"&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_proposals[2].payload"&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_proposals[2].target"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"final_output"&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;"replacement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"[REDACTED]"&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;A redacted bundle still validates clean — the whole point is that a reviewer can see &lt;em&gt;what&lt;/em&gt; was blocked and &lt;em&gt;why&lt;/em&gt;, without seeing the customer's actual email address.&lt;/p&gt;

&lt;h2&gt;
  
  
  Signing For Export Integrity
&lt;/h2&gt;

&lt;p&gt;Once a bundle is ready to leave the building, sign it so anyone downstream can tell if it was altered in transit — the redacted version, since that's the one actually going out the door:&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="nv"&gt;$ &lt;/span&gt;arb sign redacted_bundle.json &lt;span class="nt"&gt;--secret&lt;/span&gt; &lt;span class="s2"&gt;"demo-secret"&lt;/span&gt; &lt;span class="nt"&gt;--key-id&lt;/span&gt; &lt;span class="s2"&gt;"cognous-demo-key-01"&lt;/span&gt; &lt;span class="nt"&gt;--out&lt;/span&gt; signed_bundle.json
Signed bundle written to signed_bundle.json

&lt;span class="nv"&gt;$ &lt;/span&gt;arb verify signed_bundle.json &lt;span class="nt"&gt;--secret&lt;/span&gt; &lt;span class="s2"&gt;"demo-secret"&lt;/span&gt;
Signature VALID

&lt;span class="nv"&gt;$ &lt;/span&gt;arb verify signed_bundle.json &lt;span class="nt"&gt;--secret&lt;/span&gt; &lt;span class="s2"&gt;"wrong-secret"&lt;/span&gt;
Signature INVALID
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's HMAC-SHA256 over a canonical serialization of the bundle. Worth being precise about what it proves: it tells you the bundle hasn't changed since it was signed, given the secret. It does not prove &lt;em&gt;who&lt;/em&gt; signed it — there's no identity binding, no PKI, no protection if the secret itself leaks. For production use, that's a job for a real key management system. For a shared-secret export check between two parties who already trust each other, HMAC does what it says.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading The Verified File
&lt;/h2&gt;

&lt;p&gt;The auditor gets &lt;code&gt;signed_bundle.json&lt;/code&gt;, runs &lt;code&gt;verify&lt;/code&gt;, and knows immediately whether it matches what left your hands. Signing isn't encryption, though — &lt;code&gt;verify&lt;/code&gt; is the only command that speaks the signed wrapper (&lt;code&gt;signed_bundle_id&lt;/code&gt;, &lt;code&gt;replay_bundle&lt;/code&gt;, &lt;code&gt;signature_metadata&lt;/code&gt;). To actually read the run, pull the inner bundle back out first:&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;json&lt;/span&gt;
&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;signed_bundle.json&lt;/span&gt;&lt;span class="sh"&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;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;signed&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;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;extracted.json&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;w&lt;/span&gt;&lt;span class="sh"&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;f&lt;/span&gt;&lt;span class="p"&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;dump&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;signed&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;replay_bundle&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;indent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now &lt;code&gt;extracted.json&lt;/code&gt; is a plain &lt;code&gt;AgentReplayBundle&lt;/code&gt; again — &lt;code&gt;summarize&lt;/code&gt;, &lt;code&gt;validate&lt;/code&gt;, everything else works on it. Verify first, extract second: confirm nothing's been tampered with before you trust a single field inside it.&lt;/p&gt;

&lt;p&gt;The signature also doesn't prove anything about the gap between when the run happened and when you got around to signing it. &lt;code&gt;signed_at&lt;/code&gt; is just whatever time you ran the &lt;code&gt;sign&lt;/code&gt; command — there's no check against &lt;code&gt;generated_at&lt;/code&gt;. A bundle that sits unsigned for three days is three days an editor could have touched the file with no trace of it; sign it on day three, and &lt;code&gt;verify&lt;/code&gt; comes back clean regardless. The signature only covers the window it's actually been alive for. So the real answer isn't "sign it before you send it" — it's validate, redact, and sign as part of the run itself, the moment the bundle is generated, so there's no gap for anything to slip into.&lt;/p&gt;

&lt;h2&gt;
  
  
  Proving The Guardrails Held
&lt;/h2&gt;

&lt;p&gt;Back to that morning-after meeting: the complaint, and the question from the boss. What was once a stressful scavenger hunt across servers matching timestamps and logfiles is now just opening one file.&lt;/p&gt;

&lt;p&gt;Agentic runs get their guardrails from two layers: a manifest declaring what's allowed, and a control plane enforcing that live. Replay is the layer that lets your team prove those guardrails actually held — turning what the control plane recorded into one validated, signed file anyone can check independently, long after the run itself is over.&lt;/p&gt;

&lt;p&gt;Clone &lt;a href="https://github.com/cogno-us/cognous-agent-replay-bundle" rel="noopener noreferrer"&gt;Agent Replay Bundle&lt;/a&gt; and try &lt;code&gt;arb&lt;/code&gt; against the example bundles in the repo, or your own control plane exports.&lt;/p&gt;

&lt;p&gt;Learn more about Agent Replay Bundle at &lt;a href="https://cogno.us/agent_replay_bundle.html" rel="noopener noreferrer"&gt;cogno.us&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>llm</category>
      <category>security</category>
    </item>
    <item>
      <title>Cognous Control Plane: The Layer That Tells Your Agent No</title>
      <dc:creator>Doug Sillars</dc:creator>
      <pubDate>Tue, 18 Aug 2026 19:39:36 +0000</pubDate>
      <link>https://dev.to/cognous/cognous-control-plane-the-layer-that-tells-your-agent-no-3ln9</link>
      <guid>https://dev.to/cognous/cognous-control-plane-the-layer-that-tells-your-agent-no-3ln9</guid>
      <description>&lt;p&gt;It's all too common — a viral tweet describing how AI agents with poorly defined guardrails deleted production, wiped a critical database, or caused some other disastrous occurrence. Agents need guardrails: this includes permissions to do the job, but also blockers that limit the reach of the agent, thereby preventing accidental disasters that fuel viral tweets.&lt;/p&gt;

&lt;p&gt;Cognous' Open Control Stack is a framework for wrapping AI agents in guardrails, providing access where needed, but blocking the boundaries that cannot be crossed.&lt;/p&gt;

&lt;p&gt;In our &lt;a href="https://dev.to/cognous/cognous-keeps-your-ai-in-check-69l"&gt;introductory blog post&lt;/a&gt;, our agent drafted a cringey follow-up email and "helpfully" sent it to a company's entire list of top customers. Nobody approved that send, and the human marketing team had to drop everything and work to mitigate the damage.&lt;/p&gt;

&lt;p&gt;Open Control Stack is structured as four layers: &lt;strong&gt;Declare → Control → Replay → Evidence&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;We built the first layer in &lt;a href="https://dev.to/cognous/the-manifest-that-keeps-your-ai-agent-honest-3e97"&gt;The Manifest That Keeps Your AI Agent Honest&lt;/a&gt;, the &lt;strong&gt;Agent Action Manifest&lt;/strong&gt;. That's the Declare layer — a JSON file that works as the allow/block list for our agent:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pull the top customer:&lt;/strong&gt; allowed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Draft a reply:&lt;/strong&gt; draft-first&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pull contract details:&lt;/strong&gt; blocked&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Send the email:&lt;/strong&gt; needs approval&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But that's all a manifest is — a list. Nothing reads it and nothing enforces it. We need something that takes that list and actually applies it to the agent while it's running.&lt;/p&gt;

&lt;p&gt;That's the &lt;strong&gt;Agent Control Plane&lt;/strong&gt; — the Control layer. This post is about what it actually does when the agent tries something.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Sits Beside The Agent
&lt;/h2&gt;

&lt;p&gt;The Control Plane doesn't run the agent, and it isn't a framework. It sits next to whatever is already generating the agent's behavior — OpenClaw, Hermes, a custom loop, whatever — and turns every proposed action into a recorded decision before that action is treated as real.&lt;/p&gt;

&lt;p&gt;The agent's job is small in this picture — it proposes. The Control Plane does the rest: it decides, and it documents. Propose is the only row the agent touches. Decide is the gate ruling allow, block, or escalate. Document is what gets written down once the decision's made. The agent doesn't get a say in the outcome — it just gets to ask.&lt;/p&gt;

&lt;p&gt;The pattern, end to end:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvjv1xnlduxc8db6j7pjl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvjv1xnlduxc8db6j7pjl.png" alt="Cognous Control Plane Pattern" width="799" height="543"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Propose
&lt;/h3&gt;

&lt;p&gt;The agent wants to do something. The Control Plane frames it, and a proposal gets built.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Agent Task:&lt;/strong&gt; The agent kicks off the run. In our use case: "review a customer account and follow up."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Frame:&lt;/strong&gt; The Control Plane frames the execution context for the run — the task, actor, environment, allowed tools, blocked tools, policy version. This gets locked in at the start and becomes immutable for the life of the run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Action Proposal:&lt;/strong&gt; The agent's ask — this tool, this action type, this target, this payload, this reason. Recorded before anything executes — that's the point of the proposal steps. If the decision gate blocks it next, nothing downstream ever sees a real tool call, but the attempt is still on the record.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Decide
&lt;/h3&gt;

&lt;p&gt;The Control Plane decides whether that proposal is actually okay. This is where the manifest's declarations actually take effect: what it declared is what ends up in the frame's allowed and blocked lists, and the frame is what the gate checks against. Two of the six rules below check something separate from the frame: authority — whether this actor currently has permission to write, or to send things externally. Authority is granted per run, on top of the frame, and it's what rules 3 and 5 are checking for.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Policy Gate:&lt;/strong&gt; Decides what happens to the proposal. Six rules, checked top to bottom, first match wins:

&lt;ol&gt;
&lt;li&gt;tool is explicitly blocked → &lt;code&gt;block&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;tool is not in the allowed list → &lt;code&gt;escalate&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;action type is &lt;code&gt;external_send&lt;/code&gt; → &lt;code&gt;allow&lt;/code&gt; if authority for it exists, otherwise &lt;code&gt;block&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;action type is &lt;code&gt;read&lt;/code&gt; (and passed rules 1–2) → &lt;code&gt;allow&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;action type is &lt;code&gt;write&lt;/code&gt; → &lt;code&gt;allow&lt;/code&gt; if authority for it exists, otherwise &lt;code&gt;escalate&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;anything else → &lt;code&gt;escalate&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Policy Decision + Evaluation Trace:&lt;/strong&gt; The gate's verdict, plus the rule-by-rule path it took to reach it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Allow / Block / Escalate:&lt;/strong&gt; The three possible verdicts. Only &lt;code&gt;allow&lt;/code&gt; lets the proposal go on to actually touch a tool.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Document
&lt;/h3&gt;

&lt;p&gt;Once a decision is made, the Control Plane writes down what happened, what the decision was, and how it was made.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reliance Record:&lt;/strong&gt; Once an action is allowed and actually executed, a record of what the agent depended on to produce its output — a tool, a database, a file, an API, user input. Blocked and escalated actions never execute, so there's nothing to record reliance on — dependence doesn't exist until an action actually runs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run Record:&lt;/strong&gt; Every proposal, decision, trace, block, and reliance from this run, assembled into one object.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Replay Bundle:&lt;/strong&gt; The run record packaged up for later, portable inspection.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Running The Scenario For Real
&lt;/h2&gt;

&lt;p&gt;Let's take three of those actions from the manifest and actually run them through the gate: &lt;code&gt;pull_top_customers&lt;/code&gt;, &lt;code&gt;draft_reply&lt;/code&gt;, and &lt;code&gt;send_email&lt;/code&gt;. All three are on this run's allowed-tools list. &lt;code&gt;pull_contract_details&lt;/code&gt; is also declared in the frame's blocked-tools list, matching the manifest — but the task is "review a customer account and follow up," which never calls for contract details, so the agent never proposes it. It's declared, not exercised: nothing for the gate to evaluate here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Starting the run:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the Control Plane spinning up: &lt;code&gt;RunRecorder()&lt;/code&gt; creates the recorder instance, and &lt;code&gt;start_run()&lt;/code&gt; builds the frame — the same frame from the Propose section above, locking in the task, actor, environment, allowed tools, blocked tools, and policy version for everything that follows.&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;from&lt;/span&gt; &lt;span class="n"&gt;agent_control_plane&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;RunRecorder&lt;/span&gt;

&lt;span class="n"&gt;recorder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RunRecorder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;run_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;recorder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start_run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Review a customer account and follow up.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;actor&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;support-agent-v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;environment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;production&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;allowed_tools&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;pull_top_customers&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;draft_reply&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;send_email&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;blocked_tools&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;pull_contract_details&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;policy_version&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;v1.0&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Proposing and Deciding One Action:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here's the pattern for a single action, &lt;code&gt;pull_top_customers&lt;/code&gt;. The same process repeats for the other two actions — omitted here for brevity:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Propose the action with the &lt;code&gt;propose_action&lt;/code&gt; method. This is stored in the variable &lt;code&gt;a1&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Evaluate the proposal with &lt;code&gt;evaluate_action&lt;/code&gt;. This returns two values: the decision, and, if the action is blocked, a blocked-action record.
&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="n"&gt;a1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;recorder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;propose_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;tool_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;pull_top_customers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;action_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;read&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&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;crm:top_customers&lt;/span&gt;&lt;span class="sh"&gt;"&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;limit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Identify the customer&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s account tier before drafting a reply.&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="n"&gt;d1&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;recorder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;evaluate_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;pull_top_customers&lt;/code&gt; is not blocked, so we receive only the decision, and the unused second value gets thrown away with &lt;code&gt;_&lt;/code&gt;. &lt;code&gt;d1&lt;/code&gt; holds the decision: the verdict (&lt;code&gt;allow&lt;/code&gt;, &lt;code&gt;block&lt;/code&gt;, or &lt;code&gt;escalate&lt;/code&gt;), which rule produced it, and why. Here's what the gate decides across all three actions:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Rule&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pull_top_customers&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;read&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;allow&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;draft_reply&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;write&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;escalate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;send_email&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;external_send&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;block&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Missing authority is what stops two of the three from just allowing. &lt;code&gt;draft_reply&lt;/code&gt; escalates and &lt;code&gt;send_email&lt;/code&gt; blocks — same problem, nobody granted permission for this run — but different severity, because the two actions aren't equally reversible. A blocked send is stopped outright: once an email's out, it's out. An escalated write just doesn't execute yet. Nothing about the original decision ever changes — but if someone grants write authority before this run ends, a fresh proposal for the same action would clear rule 5 as &lt;code&gt;allow&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The evaluation trace is where the reasoning survives, not just the verdict. Here's the trace for the &lt;code&gt;send_email&lt;/code&gt; decision:&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;"rules_evaluated"&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="nl"&gt;"rule_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;"blocked_tool_policy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"matched"&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="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"result"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"none"&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="nl"&gt;"rule_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;"unknown_tool_policy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"matched"&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="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"result"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"none"&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;"rule_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;"external_send_authority_policy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"matched"&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="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"result"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"block"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"reason"&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 type 'external_send' requires an authority record with scope 'external_send', which was not found."&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;"final_result"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"block"&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;A reviewer looking at this trace can tell exactly which check failed and why — not just that the action was blocked. That matters because &lt;code&gt;send_email&lt;/code&gt; is on the &lt;em&gt;allowed&lt;/em&gt;-tools list; nothing about the tool itself was forbidden. The trace is what proves the block was about missing authority, not a banned tool.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Worth noting:&lt;/strong&gt; &lt;code&gt;draft_reply&lt;/code&gt; escalating isn't a lesser outcome than blocking. It's easy to read "escalate" as a downgrade of "block," but it's really the gate saying it doesn't have enough information to decide safely, so a human should. Grant &lt;code&gt;write&lt;/code&gt; authority to the run, and the same action clears rule 5 as &lt;code&gt;allow&lt;/code&gt; instead — the rule didn't change; what the actor is authorized to do did.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Documenting The Proposal And Decision
&lt;/h2&gt;

&lt;p&gt;After a decision has been made, the Cognous Control Plane documents what occurred during the process. All of the information is collated and stored in one place — not grabbed from logs scattered across different tools and pieced together by timestamp.&lt;/p&gt;

&lt;p&gt;The first step is the Reliance Record. Recall that this only documents actions that actually ran — in this case, &lt;code&gt;pull_top_customers&lt;/code&gt;:&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="n"&gt;recorder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;record_reliance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;source_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;pull_top_customers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;source_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;customer tier and account fields&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;referenced_action_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;a1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;action_id&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;p&gt;This doesn't get written to a file by itself — it's held in memory as part of the run, alongside every proposal, decision, and trace, until the whole thing gets closed out and exported:&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="n"&gt;recorder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;complete_run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Draft prepared and escalated for review. Email send was blocked, not attempted.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;recorder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;export_json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;run_record.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;bundle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;recorder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate_replay_bundle&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;export_json()&lt;/code&gt; writes everything — proposals, decisions, traces, the one blocked-action record, that one reliance record — to a &lt;code&gt;RunRecord&lt;/code&gt; JSON file. Retrieving the reliance record later is just reading that file and looking at its &lt;code&gt;reliance_records&lt;/code&gt; array.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading The Results
&lt;/h2&gt;

&lt;p&gt;Writing the record is only half the story — you also need a way to check it. That's &lt;code&gt;acp&lt;/code&gt;, the Control Plane's own command-line tool, shipped in the same repo: &lt;a href="https://github.com/cogno-us/cognous-agent-control-plane" rel="noopener noreferrer"&gt;github.com/cogno-us/cognous-agent-control-plane&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Running it against the run record we just exported:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;acp validate-run run_record.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RunRecord 79ad6860-03de-4136-9828-d4e40fbd05f4: valid (0 errors, 0 warnings)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Validation here isn't checking whether the run succeeded — it's checking that the record is internally consistent: every decision references an action that actually exists, every blocked action corresponds to a decision that actually says &lt;code&gt;block&lt;/code&gt;. It's a check on the evidence, not on the outcome.&lt;/p&gt;

&lt;p&gt;The replay bundle gets the same treatment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;acp validate-replay replay_bundle.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ReplayBundle 535c72f8-efee-4144-9b78-7f7fff73133d: valid (0 errors, 0 warnings)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What This Deliberately Doesn't Do
&lt;/h2&gt;

&lt;p&gt;The Control Plane only enforces the rules and authority it's actually been given — it doesn't infer intent, and it doesn't fill gaps with a best guess. A permission that was never granted shows up as an escalation or a block, not a pass. It's not a compliance system, and it's not a guarantee that the agent's output was correct. It's the layer that turns "the agent tried to do X" into a recorded, deterministic, inspectable decision, every time.&lt;/p&gt;

&lt;p&gt;It also doesn't decide what happens after a decision is made. The gate doesn't open a ticket, notify anyone, or track whether an escalation ever gets reviewed. Whether a human looks at the draft, whether the agent retries, whether authority eventually gets granted — that's handled by whatever's orchestrating the agent, not by the Control Plane.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next Up
&lt;/h2&gt;

&lt;p&gt;We now have a run: three proposed actions, one blocked, one escalated, one allowed, each with a trace and a fingerprint, packaged into a replay bundle that validates clean. That bundle is a record of one run.&lt;/p&gt;

&lt;p&gt;The Cognous Open Control Stack has four steps: Declare → Control → Replay → Evidence.&lt;/p&gt;

&lt;p&gt;In &lt;a href="https://dev.to/cognous/the-manifest-that-keeps-your-ai-agent-honest-3e97"&gt;The Manifest That Keeps Your AI Agent Honest&lt;/a&gt;, we declared in the manifest what was allowed and what was not. In this post, we walked through the Control Plane — it runs next to your agentic framework to allow, escalate, or block actions. The Control Plane records the results from every action taken.&lt;/p&gt;

&lt;p&gt;Next, we look at the last two steps: Replay and Evidence. These are the tools that will save you when auditors are knocking at the door, or you need to prove you've redacted customer data, or just prove it hasn't been tampered with since it was generated. That's the &lt;strong&gt;Agent Replay Bundle&lt;/strong&gt;, and we'll cover it in our next post.&lt;/p&gt;

&lt;p&gt;In the meantime, the Control Plane repo, examples, schemas, and test suite are live now: &lt;a href="https://github.com/cogno-us/cognous-agent-control-plane" rel="noopener noreferrer"&gt;github.com/cogno-us/cognous-agent-control-plane&lt;/a&gt;. Clone it, run &lt;code&gt;examples/simple_agent_run.py&lt;/code&gt;, and watch the gate make the call.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>automation</category>
      <category>security</category>
    </item>
    <item>
      <title>The Manifest That Keeps Your AI Agent Honest</title>
      <dc:creator>Doug Sillars</dc:creator>
      <pubDate>Wed, 12 Aug 2026 14:50:04 +0000</pubDate>
      <link>https://dev.to/cognous/the-manifest-that-keeps-your-ai-agent-honest-3e97</link>
      <guid>https://dev.to/cognous/the-manifest-that-keeps-your-ai-agent-honest-3e97</guid>
      <description>&lt;p&gt;In our first post, &lt;a href="https://dev.to/cognous/cognous-keeps-your-ai-in-check-69l"&gt;Cognous Keeps Your AI in Check&lt;/a&gt;, we introduced the scenario: a customer-service agent that pulls CRM records, drafts replies, and — if nobody's watching — occasionally sends 1,000 tone-deaf emails to your best clients. We said the fix isn't reading what the agent wrote after the fact. It's deciding, in advance, what the agent is even allowed to attempt.&lt;/p&gt;

&lt;p&gt;That decision has to live somewhere. It can't live in a Slack thread, a comment in the agent's system prompt, or "Dave from platform remembers we blocked that." It needs to be a file — versioned, reviewable, and boring enough that a security team can actually sign off on it.&lt;/p&gt;

&lt;p&gt;That's what Cognous's Open Control Stack is for. It's four small, open-source projects — Declare, Control, Replay, Evidence — that sit beside your agents and cover authorize, enforce, and prove. Declare comes first, and its artifact is the &lt;strong&gt;Agent Action Manifest&lt;/strong&gt;. This post describes the Action Manifest tooling in the Open Control Stack, available on &lt;a href="https://github.com/cogno-us/cognous-open-control-stack" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Four Actions, Written Down
&lt;/h2&gt;

&lt;p&gt;If you remember in post one, our AI agent was pulling customer data, drafted a cringey email, and sent it to our top 1,000 customers. The Action Manifest is used to outline the agent's permissions as declarations, to prevent this from ever happening again:&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;"manifest_version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0.1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"manifest_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"customer-service-agent-manifest"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"agent_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;"customer-service-agent"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"owner"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"support-platform-team"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"environment"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"production"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"default_action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"block"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tools"&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;"tool_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;"crm"&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;"Customer relationship management system"&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"&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="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"external_system"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"crm.internal"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"data_classification"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"customer_pii"&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;"tool_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;"email"&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;"Outbound customer email system"&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"&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="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"external_system"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"smtp.internal"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"data_classification"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"customer_pii"&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;"actions"&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;"action_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;"pull_top_customers"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"tool_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;"crm"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"action_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;"read"&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;"Retrieve top customer records from the CRM."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"default_action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"reliance_requirement"&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;"required"&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="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"allowed_source_types"&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;"tool"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"database"&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;"action_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;"draft_reply"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"tool_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;"email"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"action_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;"write"&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;"Draft a reply email for a customer inquiry."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"default_action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"authority_required"&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;"scope"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"email.draft.customer"&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;"Permission to draft outbound customer emails"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"required"&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="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"support-platform-team"&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;"review_requirement"&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;"mode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"draft_first"&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;"action_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;"pull_contract_details"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"tool_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;"crm"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"action_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;"read"&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;"Retrieve contract terms from the CRM."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"default_action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"block"&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="s2"&gt;"contract"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"restricted"&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;"action_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;"send_email"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"tool_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;"email"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"action_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;"external_send"&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;"Send an approved email to the customer."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"default_action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"escalate"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"authority_required"&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;"scope"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"email.send.customer"&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;"Outbound customer send scope"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"required"&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="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"email.send.customer"&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;"review_requirement"&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;"mode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"approval_required"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"reviewer_role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"support-lead"&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;"reliance_requirement"&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;"required"&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="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"allowed_source_types"&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;"tool"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user_input"&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;"payload_policy"&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;"sensitive_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;"customer_email"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"customer_name"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"forbidden_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;"contract_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;"redaction_hints"&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;"field_path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"customer_email"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"reason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PII in exported records"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(Field names here follow the public schema — check the &lt;a href="https://github.com/cogno-us/cognous-open-control-stack" rel="noopener noreferrer"&gt;repo&lt;/a&gt; for the exact current shape if you're implementing against it.)&lt;/p&gt;

&lt;p&gt;Rules are only as good as what is defined. At the very top of the manifest, we explicitly cover anything not mentioned: &lt;code&gt;"default_action": "block"&lt;/code&gt;. So when a new tool is onboarded (for example &lt;code&gt;send_tweet&lt;/code&gt;), you don't need to go back and update every manifest — it's blocked until you expressly give the agent permission to use it.&lt;/p&gt;

&lt;p&gt;For those actions that are defined:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;pull_top_customers&lt;/code&gt; and &lt;code&gt;draft_reply&lt;/code&gt; are both &lt;code&gt;allow&lt;/code&gt;, but not the same kind of allow: pulling the top customers is allowed, full stop, but the draft reply has &lt;code&gt;review_requirement.mode: draft_first&lt;/code&gt; — the agent can write it, but it stops there.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;send_email&lt;/code&gt; isn't just "escalate" as a vague gesture — it names the exact authority scope required (&lt;code&gt;email.send.customer&lt;/code&gt;), the exact reviewer role (&lt;code&gt;support-lead&lt;/code&gt;), and which payload fields have to be redacted before this record goes anywhere near an audit export.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pull_contract_details&lt;/code&gt; is the odd one out — it doesn't strictly need to be there. &lt;code&gt;default_action: block&lt;/code&gt; already covers anything undeclared, so an unlisted "read contract details" action would be blocked anyway. It's included for visibility: a blocked action that shows up in the manifest, tagged &lt;code&gt;restricted&lt;/code&gt;, is a documented decision someone can point to. A blocked action that's simply never mentioned looks identical to an oversight.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's important to note that the manifest is not enforcement. It doesn't stop the agent from doing anything — a runtime layer has to actually read it and act on it (that'll be described in the next post). What the manifest gives you is a single artifact that says, in one place, what &lt;em&gt;should&lt;/em&gt; happen — before the agent has run even once.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Nine Action Types
&lt;/h2&gt;

&lt;p&gt;In our simple manifest above, we show three possible action types. The manifest supports nine. These classifications are what give a governance review the ability to distinguish a harmless read from something that should never happen without a human:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Action&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;&lt;code&gt;read&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;retrieves or inspects information&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;write&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;creates or modifies information, not necessarily sent externally&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;external_send&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;sends information outside the system boundary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;delete&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;removes information or records&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;export&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;packages or transfers information downstream&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;purchase&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;initiates or prepares a purchase&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;approve&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;approves or authorizes a workflow step&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;escalate&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;routes to a human or supervisory process&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;other&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;anything not otherwise classified&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A tool list alone can't tell you this. Knowing the agent "has access to email" tells you nothing about whether it can draft, send, or both, and what's supposed to happen before each.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Four Review Modes
&lt;/h2&gt;

&lt;p&gt;Action type says &lt;em&gt;what kind&lt;/em&gt; of thing an action is. Review mode says &lt;em&gt;who has to look at it, and when&lt;/em&gt; — and it's doing just as much governance work in our example as action type is. There are four:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mode&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;&lt;code&gt;none&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;no specific review posture declared&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;human_review&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;a human is expected to look at it before execution or completion — no specific person named&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;approval_required&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;approval is required from a &lt;strong&gt;named reviewer role&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;draft_first&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the agent produces a draft; it does not finish the action itself&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The difference between &lt;code&gt;human_review&lt;/code&gt; and &lt;code&gt;approval_required&lt;/code&gt; is easy to miss but it matters: &lt;code&gt;human_review&lt;/code&gt; says "someone should look at this" without saying who. &lt;code&gt;approval_required&lt;/code&gt; is stricter — it names a reviewer role, and the validator enforces that: an &lt;code&gt;approval_required&lt;/code&gt; action with no &lt;code&gt;reviewer_role&lt;/code&gt; fails validation outright.&lt;/p&gt;

&lt;p&gt;That's why &lt;code&gt;send_email&lt;/code&gt; in our manifest uses &lt;code&gt;approval_required&lt;/code&gt; with &lt;code&gt;reviewer_role: "support-lead"&lt;/code&gt;, not &lt;code&gt;human_review&lt;/code&gt;. Sending customer email isn't "someone should probably glance at this" — it's "this specific role signs off, or it doesn't go out."&lt;/p&gt;

&lt;p&gt;Worth being precise about what &lt;code&gt;"support-lead"&lt;/code&gt; actually is here: a role string, nothing more. The manifest doesn't know who holds that role, doesn't know they're on Slack, and doesn't page anyone. Your enterprise-side plumbing converts "support lead" to Janice, and builds an alerting system in Slack to let her know that her approval is required. The public stack's job ends at "this role must approve, and there's a record of whether it did."&lt;/p&gt;

&lt;h2&gt;
  
  
  Reliance: A Third Thing Being Tracked
&lt;/h2&gt;

&lt;p&gt;Authority asks whether an action was &lt;em&gt;allowed&lt;/em&gt;. Review asks whether a &lt;em&gt;person&lt;/em&gt; signed off. Reliance asks something different: what did this action actually &lt;em&gt;depend on&lt;/em&gt; to produce its result?&lt;/p&gt;

&lt;p&gt;A &lt;code&gt;reliance_requirement&lt;/code&gt; declares that an action should leave a record of its source — a tool, a database, a file, an API, or user input. &lt;code&gt;pull_top_customers&lt;/code&gt; declares reliance on &lt;code&gt;tool&lt;/code&gt; and &lt;code&gt;database&lt;/code&gt;, because a governance review should be able to see it actually came from the CRM and not somewhere the agent invented. &lt;code&gt;send_email&lt;/code&gt; declares reliance on &lt;code&gt;tool&lt;/code&gt; and &lt;code&gt;user_input&lt;/code&gt;, tying the send back to the approved draft it was built from.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;draft_reply&lt;/code&gt; and &lt;code&gt;pull_contract_details&lt;/code&gt; don't declare a reliance requirement — which is why the summarize output above shows &lt;code&gt;Actions requiring reliance: 2&lt;/code&gt;, not 4.&lt;/p&gt;

&lt;h2&gt;
  
  
  Validation Isn't Optional, and It Isn't Decoration
&lt;/h2&gt;

&lt;p&gt;A manifest can be valid JSON, but be semantic nonsense. A &lt;code&gt;delete&lt;/code&gt; action on the production database marked &lt;code&gt;allow&lt;/code&gt; isn't caught by JSON Schema. It's caught by the validator, which checks things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;does every action reference a tool that's actually declared and allowed?&lt;/li&gt;
&lt;li&gt;is a privileged action type (&lt;code&gt;external_send&lt;/code&gt;, &lt;code&gt;write&lt;/code&gt;, &lt;code&gt;delete&lt;/code&gt;, &lt;code&gt;purchase&lt;/code&gt;, &lt;code&gt;approve&lt;/code&gt;) missing an authority requirement it should have?&lt;/li&gt;
&lt;li&gt;is an action marked &lt;code&gt;approval_required&lt;/code&gt; but missing a reviewer role?&lt;/li&gt;
&lt;li&gt;are there duplicate action or tool names?&lt;/li&gt;
&lt;li&gt;are sensitive fields declared with no redaction hints to match?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Run it from the CLI — &lt;code&gt;aam&lt;/code&gt;, short for Agent Action Manifest, is the command-line tool that ships with the manifest repo. First we run the validate command to ensure that the JSON is valid:&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="nv"&gt;$ &lt;/span&gt;aam validate customer-service-agent-manifest.json
Validation result: VALID
Manifest ID:       customer-service-agent-manifest

Warnings &lt;span class="o"&gt;(&lt;/span&gt;3&lt;span class="o"&gt;)&lt;/span&gt;:
  &lt;span class="o"&gt;[&lt;/span&gt;W004] &lt;span class="o"&gt;[&lt;/span&gt;actions[1]&lt;span class="o"&gt;(&lt;/span&gt;draft_reply&lt;span class="o"&gt;)]&lt;/span&gt; Action &lt;span class="s1"&gt;'draft_reply'&lt;/span&gt; has effective default_action &lt;span class="s1"&gt;'allow'&lt;/span&gt; &lt;span class="k"&gt;for &lt;/span&gt;action_type &lt;span class="s1"&gt;'write'&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
  &lt;span class="o"&gt;[&lt;/span&gt;W006] &lt;span class="o"&gt;[&lt;/span&gt;actions[1]&lt;span class="o"&gt;(&lt;/span&gt;draft_reply&lt;span class="o"&gt;)&lt;/span&gt;.payload_policy] Action &lt;span class="s1"&gt;'draft_reply'&lt;/span&gt; has action_type &lt;span class="s1"&gt;'write'&lt;/span&gt; but no payload_policy is declared.
  &lt;span class="o"&gt;[&lt;/span&gt;W005] &lt;span class="o"&gt;[&lt;/span&gt;actions[2]&lt;span class="o"&gt;(&lt;/span&gt;pull_contract_details&lt;span class="o"&gt;)&lt;/span&gt;.reliance_requirement] Action &lt;span class="s1"&gt;'pull_contract_details'&lt;/span&gt; has action_type &lt;span class="s1"&gt;'read'&lt;/span&gt; but no reliance_requirement is declared.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;VALID, but not silent — the validator still flags things worth a second look, even in a manifest that passes. Fair warnings, too: we didn't bother declaring a payload policy for a draft-only action, and a blocked action doesn't need reliance evidence it'll never produce.&lt;/p&gt;

&lt;p&gt;Next we can summarize the manifest with the summarize command:&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="nv"&gt;$ &lt;/span&gt;aam summarize customer-service-agent-manifest.json
Manifest ID:          customer-service-agent-manifest
Agent name:           customer-service-agent
Environment:          production
Tools:                2
Actions:              4

Actions by &lt;span class="nb"&gt;type&lt;/span&gt;:
  external_send: 1
  &lt;span class="nb"&gt;read&lt;/span&gt;: 2
  write: 1

Actions by default posture:
  allow: 2
  block: 1
  escalate: 1

Actions requiring authority: 2
Actions requiring review:    2
Actions requiring reliance:  2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That "2" next to authority is easy to misread as just the send — it's actually &lt;code&gt;draft_reply&lt;/code&gt; and &lt;code&gt;send_email&lt;/code&gt; both. Drafting on a customer's behalf declares its own authority requirement (&lt;code&gt;email.draft.customer&lt;/code&gt;), separate from the one on the final send (&lt;code&gt;email.send.customer&lt;/code&gt;). Declaring it isn't the same as enforcing it — nothing here checks whether that authority actually exists. That check is the Control Plane's job, next post.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;aam&lt;/code&gt; verifies that your manifest is ready, and gives the team a concise summary of what's allowed.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Deliberately Doesn't Do
&lt;/h2&gt;

&lt;p&gt;The manifest is the &lt;strong&gt;Declare&lt;/strong&gt; layer, and it stays in its lane:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;it doesn't execute the agent&lt;/li&gt;
&lt;li&gt;it doesn't enforce anything at runtime&lt;/li&gt;
&lt;li&gt;it doesn't grant authority — &lt;code&gt;email.send.customer&lt;/code&gt; being &lt;em&gt;declared&lt;/em&gt; as required doesn't mean it &lt;em&gt;exists&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;it doesn't replace your application's own authorization system&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Declaring that sending email should require approval is not the same thing as stopping an unapproved send. That gap — between what's declared and what actually happens when the agent tries to act — is exactly what the next layer closes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next up
&lt;/h2&gt;

&lt;p&gt;The manifest says what the agent &lt;em&gt;may&lt;/em&gt; propose. It says nothing about what the agent actually &lt;em&gt;does&lt;/em&gt; on a Tuesday afternoon when it's mid-run and reaching for the send button. That's the &lt;strong&gt;Agent Control Plane&lt;/strong&gt; — it reads this exact manifest, sits beside the agent at runtime, and turns every proposed action into a recorded decision: allow, block, or escalate, deterministically, every time. That's next.&lt;/p&gt;

&lt;p&gt;The manifest repo, examples, schemas, and test suite are live now: &lt;a href="https://github.com/cogno-us/cognous-open-control-stack" rel="noopener noreferrer"&gt;github.com/cogno-us/cognous-open-control-stack&lt;/a&gt;. Clone it, validate the customer-service example, break it on purpose and watch the validator catch you.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>automation</category>
      <category>security</category>
    </item>
    <item>
      <title>Cognous Keeps Your AI in Check</title>
      <dc:creator>Doug Sillars</dc:creator>
      <pubDate>Thu, 06 Aug 2026 22:49:57 +0000</pubDate>
      <link>https://dev.to/cognous/cognous-keeps-your-ai-in-check-69l</link>
      <guid>https://dev.to/cognous/cognous-keeps-your-ai-in-check-69l</guid>
      <description>&lt;p&gt;You've probably gotten an email that was supposed to feel personal, but obviously wasn't. Something about it just felt off. Automated. And somewhere in the back of your mind, you wondered: who approved this?&lt;/p&gt;

&lt;p&gt;Honestly? The answer is probably no one.&lt;/p&gt;

&lt;p&gt;Organizations are leaning harder on AI agents to actually do work. Agents are making database queries, writing letters, accessing email lists, but where does the access end? Without solid guardrails, tone-deaf emails are no longer an embarrassing edge case, they're a real risk. Nobody wants to be the team that let an agent send 1,000 tone-deaf emails to its best clients before anyone noticed.&lt;/p&gt;

&lt;p&gt;This series is about the tooling that keeps that from happening, and about actually building it, not just reading about it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually needs to happen
&lt;/h2&gt;

&lt;p&gt;The email itself might be perfectly fine. Correct grammar, on-brand, even genuinely well-written. But the problem is that the email didn't need to be sent. The agent just did it anyway, without any guidance from the team.&lt;/p&gt;

&lt;p&gt;So the fix isn't a better read of what the agent said. It's governance around what the agent is allowed to do, three capabilities, specifically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Authorize&lt;/strong&gt;: Decide in advance what an agent is allowed to do.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enforce&lt;/strong&gt;: Apply that decision the moment the agent tries to act, before the action executes, not after.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prove&lt;/strong&gt;: Turn every run into a record you can produce on demand, not one you have to reconstruct from memory.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Application logs are a day late and a dollar short: the emails are already sent. Looking at logs is reactive. You need a stack that sits in front of your agents and proactively approves or denies actions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introducing Cognous
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://cogno.us/" rel="noopener noreferrer"&gt;Cognous&lt;/a&gt; builds infrastructure for governing AI agents. Its first release, the Open Control Stack, is fully open source: four small projects that sit beside your AI agents to keep them in check, together covering Authorize, Enforce, and Prove. The four projects are &lt;strong&gt;Declare, Control, Replay, Evidence&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Declare, the Agent Action Manifest&lt;/strong&gt;. Written once, before the agent ever runs. The manifest provides or denies access. The agent is attempting to complete a task, but the manifest keeps it in check:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;Pull top customers&lt;/em&gt;: &lt;code&gt;default_action: allow&lt;/code&gt;. A CRM read, flagged to leave a reliance record of what data it used.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Draft a reply&lt;/em&gt;: &lt;code&gt;default_action: allow&lt;/code&gt;, review_mode: draft_first. The agent prepares content; it doesn't finish the job.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Pull contract details&lt;/em&gt;: &lt;code&gt;default_action: block&lt;/code&gt;. Off-limits for this agent, full stop.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Send the email&lt;/em&gt;: &lt;code&gt;default_action: escalate&lt;/code&gt;. Requires a named authority scope and human sign-off before it's eligible to execute.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The point is that the manifest lets you say &lt;em&gt;reading customer data is fine, reading contracts is not, drafting is fine, but sending the email needs a person&lt;/em&gt;: four different rules for four different actions, declared before any of them happen, instead of one blanket policy applied to "the agent" as if every action carried the same risk.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Control, the Agent Control Plane&lt;/strong&gt;. Runs live, alongside the agent. Every time the agent tries to do something, the Control Plane checks it against what was declared, records the attempt, and either allows it, blocks it, or escalates it for approval, deterministically, every time. Every one of those outcomes gets written down, not just the bad ones.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Replay, the Agent Replay Bundle&lt;/strong&gt;. Not a process, an export. When the run ends, everything the Control Plane recorded (what was proposed, what was decided, what was escalated, what it relied on) gets packaged into a single portable file. No more digging through the logs of a dozen systems to reconstruct one run. Every read, every draft, every escalation, everything that was allowed or held back: it's all in one place.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Evidence, the Agent Governance Evidence Pack&lt;/strong&gt;. Assembled later, across many runs. This is where you get to show your work: not "trust us, we have controls," but "here's proof: 40 sends needed sign-off this month, and here's what happened to each one."&lt;/p&gt;

&lt;h2&gt;
  
  
  Walking the scenario through the stack
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;The manifest declares that sending an email requires authority and escalates rather than executing automatically, decided before the agent ever runs a single task.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Mid-run, the Control Plane checks each of the agent's proposals against the manifest as they happen: pulling top customers and drafting a reply go through, pulling contract details is blocked outright, and the proposed send doesn't execute, it escalates and gets held for a person to review. Every one of those outcomes gets recorded as it happens, not just the one that got stopped.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;When the run ends, every step (the reads, the draft, the block, the held send, and the outcome) gets packaged into a replay bundle.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Over time, those bundles roll up into an evidence pack: not "we think our agent behaves," but "here's the record of every send this month that needed sign-off, and what happened to it."&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A thousand cringey emails to customers drops to zero. There are no questions about why, no furious Slack thread trying to piece it together. And if anyone does wonder, the stack's replay bundle provides the paper trail.&lt;/p&gt;

&lt;h2&gt;
  
  
  See it yourself
&lt;/h2&gt;

&lt;p&gt;The Open Control Stack, Cognous's first release, is fully open source, and everything above (the manifest fields, the allow/block/escalate decisions, all of it) comes straight from the actual code, not a slide deck. If you want to see AI agent governance in action instead of taking our word for it, the manifest, control plane, replay bundle, and evidence pack repos are all live on &lt;a href="https://github.com/cogno-us/cognous-open-control-stack" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. Clone one, run the examples, watch a proposed action get authorized, blocked, or escalated in real time. You can learn more about Cognous at &lt;a href="https://cogno.us/" rel="noopener noreferrer"&gt;cogno.us&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This is the first post in a series on AI agent governance. Next up, we build the manifest from this exact scenario: the real schema, the real validation rules, and what runtime authorization for AI agents actually looks like as working code. From there we'll get into the AI agent control plane that enforces it and how a single run turns into replayable AI agent evidence you can hand to an auditor. More posts coming soon.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>automation</category>
      <category>security</category>
    </item>
    <item>
      <title>When should you use canary deployments?</title>
      <dc:creator>Doug Sillars</dc:creator>
      <pubDate>Mon, 20 Apr 2026 18:42:22 +0000</pubDate>
      <link>https://dev.to/coderabbitai/when-should-you-use-canary-deployments-2ch9</link>
      <guid>https://dev.to/coderabbitai/when-should-you-use-canary-deployments-2ch9</guid>
      <description>&lt;p&gt;One solution for a tricky or high-risk deployment is to leverage a canary deployment. In a canary deployment, new features are rolled out to a small subset of your customer base with no fanfare. If things go well: hooray! Canary deployments are ideal for times when a deployment might go sideways or the feature might not work as expected. By utilizing a canary release, you have minimized the risk, as impacting a small fraction of your users is a lot better than affecting all of them.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvx8m8c1v3v3l5p1xt6w2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvx8m8c1v3v3l5p1xt6w2.png" alt="Deploying a Canary Release" width="512" height="236"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This post will discuss when development teams might choose to use canary deployments in their applications, what is a canary deployment, common pitfalls to avoid, and how modern tooling can help ease the rollout of canary deployments.&lt;/p&gt;

&lt;h1&gt;
  
  
  What is a canary deployment?
&lt;/h1&gt;

&lt;p&gt;Where does the term canary deployment come from? You may have heard of the term "canary in the coal mine." Early coal miners would keep caged songbirds in the mine with them, because these small birds are highly susceptible to toxic gases. If the canaries suddenly died, the miners knew that there was a dangerous gas release, and they could evacuate the mine before they succumbed to the deadly gas.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgejryztof28vo7uoxlmq.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgejryztof28vo7uoxlmq.jpg" alt="A canary in a coal mine" width="512" height="512"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A canary deployment can be performed when there is concern about the risks involved. Perhaps you are rolling out new cloud infrastructure that has not yet been tested with the production environment, or maybe there are performance concerns—the updates might cause the website's performance to drop to a terrible crawl. Perhaps the change is small, but it could not be tested for security vulnerabilities. A failed rollout to your entire customer base could end in disaster in all of these cases.&lt;/p&gt;

&lt;h1&gt;
  
  
  Is your team already using canary deployments?
&lt;/h1&gt;

&lt;p&gt;Many teams practice canary-style releases without explicitly calling them that. Common variations include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Blue-green deployments&lt;/strong&gt;: Two production environments run in parallel. The blue environment serves the stable version, while green runs the new version. Traffic is gradually shifted from blue to green, limiting risk and enabling fast rollback.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Experimental deployments&lt;/strong&gt;: New behavior is exposed to a defined group of users, often with explicit awareness or opt-in. These are typically broader than canary deployments and focus on measuring user impact rather than system health alone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gradual rollouts (progressive delivery)&lt;/strong&gt;: After validating a release with a small subset of users, traffic is incrementally increased (for example, 5% → 25% → 100%) while monitoring key metrics like error rates and latency.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shadow deployments&lt;/strong&gt;: The new version runs in parallel and receives a copy of real production traffic, but responses are not served to users. This is useful for validation and performance testing without customer impact.&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Planning a canary deployment
&lt;/h1&gt;

&lt;p&gt;Imagine this situation: You are about to launch a new feature that is considered high-risk for some reason. The team has opted for a canary deployment, releasing it to a limited group of users first. Achieving a successful deployment in this scenario demands meticulous planning.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Deployment audience&lt;/strong&gt;: Who gets the update? This requires a basic knowledge of your audience and understanding of who would benefit the most from the change. If your new version is an overhaul of the mobile dashboard, and the canary release is delivered to desktop users, how can you determine if the deployment was successful? It is certainly useful to have some desktop users in the cohort (in case something breaks on desktop), but in order to best understand the results of the deployment, you want to ensure that users interact with the features of the new version.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Duration&lt;/strong&gt;: How long will the test run? An hour? A week? Depending on the type of deployment, you may know the success/failure quickly. (Did the database correctly sync with the servers?). However, sometimes it may take days to determine success. (Is website performance faster during busy periods?).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Metrics&lt;/strong&gt;: What will the team track during the deployment? What metrics will be considered a success? What metrics would be branded a partial success? When does the team admit failure and roll back the deployment to fix bugs?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Feature flags&lt;/strong&gt;: For some canary deployments, using feature flags can be helpful. Feature flags are tools in your code that manage the deployment of application features to specific audiences. If your team is planning to use canary deployment extensively, it may be useful to integrate feature flags into your codebase.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Deployment risks
&lt;/h1&gt;

&lt;p&gt;With any release, there is a risk of failure. Using a canary deployment mitigates the risk by reducing the blast radius of a failure to a small subset of users—however, even canary releases have risks. Among these are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Service outages&lt;/strong&gt;: If the deployment goes bad, the application may slow down or even cease to work for the selected users.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security breaches&lt;/strong&gt;: The new release may introduce a security vulnerability that was not discovered in testing.
Rollback mechanism: Many canary releases do not have a formalized rollback plan, should things go wrong.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data loss&lt;/strong&gt;: If the canary rollout is using a new database or a new interface to the database, any issue with the rollout may introduce data inconsistencies or data loss.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For any release, it is important to consider the risks and how they will be mitigated.  While the canary release lowers the risks, having plans for rollback, handling data loss etc. should be made prior to the release.&lt;/p&gt;

&lt;h1&gt;
  
  
  Key metrics during canary deployments
&lt;/h1&gt;

&lt;p&gt;During any release, it is critical to monitor your systems in case of any trouble during the deployment. Canary releases are no different, although the experimental nature of the release means there is a higher likelihood of issues arising:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Error rates&lt;/strong&gt;: Are your systems throwing more errors than typically seen in production?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;System metrics&lt;/strong&gt;: Keep a close eye on memory, CPU, database, and API queries, and other resource metrics. We've all been a part of a release where the new code introduced a memory leak, or suddenly API usage was pegging 400% of normal. By monitoring these in the canary release, the issues can be quickly remediated for the canary users before releasing to the entire base.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Traffic distribution&lt;/strong&gt;: Is the canary release being properly served to the selected users? Are users accidentally crossing between the two environments? Handling these issues early ensures that the new build is completely isolated from the old environment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security metrics&lt;/strong&gt;: Is the login behavior different in the canary build? Are users interacting with this build differently than the previous version? Could this imply a potential threat?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Monitoring these metrics during the deployment and after the release can help your team better understand the wins (and potential improvements to your app) that occur during the canary release. In addition to traditional monitoring, there are now AI tools that can be used to compare your two environments, quickly parsing the data and determining differences from the old version and the new software version that might be difficult to see in traditional logging.&lt;/p&gt;

&lt;h1&gt;
  
  
  Canary deployments: What's next?
&lt;/h1&gt;

&lt;p&gt;When dealing with a high-risk release, a canary deployment to a small subset of users is a great way to mitigate exposure in production. To be successful in your canary release, be sure to discuss how you will choose the cohort of users, monitor the release, and plan a rollback strategy prior to the release. Then, during the deployment process, your team can track the essential metrics to understand if the canary deployment was a success. Should the deployment go well, the team can plan to add users to the new version of the software, slowly deprecating the old version.&lt;/p&gt;

&lt;p&gt;Developers who use canary releases for risky deployments are able to test problematic code or infrastructure to a small user base, pushing the product ahead while mitigating the risk of an outage.&lt;/p&gt;

</description>
      <category>cicd</category>
      <category>devops</category>
      <category>sre</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Database branching in Django apps using GitHub actions</title>
      <dc:creator>Doug Sillars</dc:creator>
      <pubDate>Mon, 02 Dec 2024 18:36:27 +0000</pubDate>
      <link>https://dev.to/hackmamba/database-branching-in-django-apps-using-github-actions-3lgh</link>
      <guid>https://dev.to/hackmamba/database-branching-in-django-apps-using-github-actions-3lgh</guid>
      <description>&lt;p&gt;Creating online previews of your applications is a great way to test that all the required functionality is present. When building and testing a dev build of your application from a pull request (PR), the last thing you want is for your tests to affect your production database.  Using a test branch of the production database ensures that the production database remains untouched, ensuring no accidental deletion of data or adding test data into the production database.&lt;/p&gt;

&lt;p&gt;In this post, we’ll create a set of GitHub Actions to automate the testing process of a pull request. Our GitHub Action will run when the pull request is created, generate a test branch of the production database, and deploy the code to Digital Ocean.  Once the PR is merged, a second GitHub Action will rebuild the Digital Ocean app with production code (and database), and the test database branch will be deleted.&lt;/p&gt;

&lt;p&gt;By finishing this article, you’ll be able to automate the creation of app previews using NeonDB branches, Digital Ocean, and Django. Let’s jump right in!&lt;/p&gt;

&lt;h1&gt;
  
  
  Setup
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Code repo
&lt;/h2&gt;

&lt;p&gt;To begin, we’ll use the &lt;a href="https://github.com/evanshortiss/django-neon-quickstart" rel="noopener noreferrer"&gt;Django Neon&lt;/a&gt; &lt;a href="https://github.com/evanshortiss/django-neon-quickstart" rel="noopener noreferrer"&gt;q&lt;/a&gt;&lt;a href="https://github.com/evanshortiss/django-neon-quickstart" rel="noopener noreferrer"&gt;uickstart&lt;/a&gt; repo on GitHub. Create a fork and clone your fork locally.  Follow the setup instructions in the README, and run the application locally to ensure it is up and running. (You’ll need &lt;a href="https://console.neon.tech/" rel="noopener noreferrer"&gt;a NeonDB account&lt;/a&gt; to add the environmental variables that connect to NeonDB and power the app.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Digital Ocean
&lt;/h2&gt;

&lt;p&gt;Digital Ocean has a simplified process for launching applications on its platform. You can be up and running in minutes with just a few configuration steps.&lt;/p&gt;

&lt;p&gt;You’ll need an account at &lt;a href="https://cloud.digitalocean.com/" rel="noopener noreferrer"&gt;Digital Ocean&lt;/a&gt; to deploy your application. From the Digital Ocean dashboard, select “Create” → App Platform.  Connect to GitHub, and choose the django-neon-quickstart, branch main. Click next. I ran this on the $5/month instance. In the “build phase,” click edit and add the three commands below:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pip install -r requirements.txt
python manage.py makemigrations
python manage.py migrate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;These are the steps you ran to get the repo running locally—we’re just repeating it on Digital Ocean. &lt;/p&gt;

&lt;p&gt;Update the “Run” command to use gunicorn:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;gunicorn django_neon.wsgi:application --bind 0.0.0.0:8000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Finally, update the HTTP Port to 8000 to match the port in the repository.  &lt;/p&gt;

&lt;p&gt;In step two of the setup process, update the environmental variables. You can do a bulk upload and copy and paste in your &lt;code&gt;.env&lt;/code&gt; from the local repo as shown below:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbjeaks4y1j7kag461eot.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbjeaks4y1j7kag461eot.png" width="800" height="593"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click through the rest of the commands, and on completion, the app will deploy and be available for use on the internet.&lt;/p&gt;

&lt;p&gt;Note that in this repository the deployed code is the &lt;code&gt;main&lt;/code&gt; branch. We would like to display the PR preview from the &lt;em&gt;dev&lt;/em&gt; branch. (Change the names &lt;em&gt;main&lt;/em&gt; and &lt;em&gt;dev&lt;/em&gt; to whatever branches you wish to preview.)  Let’s continue our setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  GitHub Secrets
&lt;/h2&gt;

&lt;p&gt;We need to add three GitHub secrets to the repository. Add secrets by clicking “Settings” on the top menu. Then: “Secrets and Variables” → Actions, and add repository secrets.&lt;/p&gt;

&lt;p&gt;Here are the three secrets to be added:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;em&gt;DO_KEY&lt;/em&gt;&lt;/strong&gt;&lt;strong&gt;:&lt;/strong&gt; A key from Digital Ocean with scopes to create, read, update, and delete apps.  You can create this from the Digital Ocean dashboard under API.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;em&gt;NEON_API_KEY&lt;/em&gt;&lt;/strong&gt;&lt;strong&gt;:&lt;/strong&gt; Create your Neon API key at the NEON dashboard. Click your Avatar in the upper right corner, select Account Settings, and then choose API keys to create an API key.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;em&gt;NEON_PW&lt;/em&gt;&lt;/strong&gt;&lt;strong&gt;:&lt;/strong&gt; This is the &lt;code&gt;PGPASSWORD&lt;/code&gt; from the .env file.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Digital Ocean app spec files
&lt;/h2&gt;

&lt;p&gt;The App Spec file defines how the build process will be run at Digital Ocean. You can find your App Spec file in your App’s dashboard under “Settings.” (It will be autogenerated when you create your project.)&lt;/p&gt;

&lt;p&gt;In your GitHub Repository, create a &lt;code&gt;.do&lt;/code&gt; directory, and make two copies of the App Spec file: &lt;em&gt;app.yaml&lt;/em&gt; and &lt;em&gt;default.yaml&lt;/em&gt;. These will be used by our GitHub Actions to edit the Digital Ocean Application.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;app.yaml is what will be provisioned on a pull request being opened:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;    &lt;span class="na"&gt;alerts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;rule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;DEPLOYMENT_FAILED&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;rule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;DOMAIN_FAILED&lt;/span&gt;
    &lt;span class="na"&gt;features&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;buildpack-stack=ubuntu-22&lt;/span&gt;
    &lt;span class="na"&gt;ingress&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;component&lt;/span&gt;&lt;span class="pi"&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;django-neon-quickstart&lt;/span&gt;
        &lt;span class="na"&gt;match&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;prefix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="err"&gt;*&lt;/span&gt;&lt;span class="nv"&gt;*seal-app-dev&lt;/span&gt;&lt;span class="err"&gt;**&lt;/span&gt;
    &lt;span class="na"&gt;region&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nyc&lt;/span&gt;
    &lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;build_command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|-&lt;/span&gt;
        &lt;span class="s"&gt;pip install -r requirements.txt&lt;/span&gt;
        &lt;span class="s"&gt;python manage.py makemigrations&lt;/span&gt;
        &lt;span class="s"&gt;python manage.py migrate&lt;/span&gt;
      &lt;span class="na"&gt;environment_slug&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;python&lt;/span&gt;
      &lt;span class="na"&gt;envs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PGHOST&lt;/span&gt;
        &lt;span class="na"&gt;scope&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;RUN_AND_BUILD_TIME&lt;/span&gt;
        &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="err"&gt;*&lt;/span&gt;&lt;span class="nv"&gt;*new_host&lt;/span&gt;&lt;span class="err"&gt;**&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PGDATABASE&lt;/span&gt;
        &lt;span class="na"&gt;scope&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;RUN_AND_BUILD_TIME&lt;/span&gt;
        &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;neondb&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PGUSER&lt;/span&gt;
        &lt;span class="na"&gt;scope&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;RUN_AND_BUILD_TIME&lt;/span&gt;
        &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;neondb_owner&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PGPASSWORD&lt;/span&gt;
        &lt;span class="na"&gt;scope&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;RUN_AND_BUILD_TIME&lt;/span&gt;
        &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="err"&gt;*&lt;/span&gt;&lt;span class="nv"&gt;*new_password&lt;/span&gt;&lt;span class="err"&gt;**&lt;/span&gt;
      &lt;span class="na"&gt;github&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;branch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="err"&gt;*&lt;/span&gt;&lt;span class="nv"&gt;*dev&lt;/span&gt;&lt;span class="err"&gt;**&lt;/span&gt;
        &lt;span class="na"&gt;deploy_on_push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
        &lt;span class="na"&gt;repo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dougsillars/django-neon-quickstart&lt;/span&gt;
      &lt;span class="na"&gt;http_port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8000&lt;/span&gt;
      &lt;span class="na"&gt;instance_count&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
      &lt;span class="na"&gt;instance_size_slug&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;apps-s-1vcpu-1gb-fixed&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;django-neon-quickstart&lt;/span&gt;
      &lt;span class="na"&gt;run_command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gunicorn django_neon.wsgi:application --bind 0.0.0.0:8000&lt;/span&gt;
      &lt;span class="na"&gt;source_dir&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are four changes made in this file from the original at Digital Ocean:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;name: add “-dev” to the end of the app name.&lt;/li&gt;
&lt;li&gt;PGHOST: Value should be &lt;code&gt;new_host&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;PGPASSOWRD:value should be &lt;code&gt;new_password&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;github:branch &lt;code&gt;dev&lt;/code&gt; replaces main.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;When our GitHub Action runs:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The name of the application will change in the Digital Ocean dashboard, allowing the dev to see that the current state is a dev state.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;new_host&lt;/em&gt; and &lt;em&gt;new_password&lt;/em&gt; will be programmatically updated with the values from our newly created NeonDB branch.
&lt;/li&gt;
&lt;li&gt;We want to deploy the dev branch of our code to Digital Ocean to see the changes.
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;default.yaml is used to revert the App Spec to production.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The &lt;em&gt;default.yaml&lt;/em&gt; has two changes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;name:  Add “-prod” to the end of the name.&lt;/li&gt;
&lt;li&gt;PGPASSWORD:value: &lt;code&gt;new_password&lt;/code&gt; replaces the password. &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;(We’re not showing the &lt;a href="https://github.com/dougsillars/django-neon-quickstart/blob/main/.do/default.yaml" rel="noopener noreferrer"&gt;entire file&lt;/a&gt; here for space reasons.)&lt;/p&gt;

&lt;p&gt;This will change the name in the Digital Ocean dashboard to show that prod is visible.  The password will revert to the original password from the primary branch of the database, and since the branch is main, the build will be the main branch.&lt;/p&gt;

&lt;p&gt;With these changes, we are now ready to begin implementing our two GitHub Actions: “Create NeonDB Branch” and “Destroy NeonDB Branch.”&lt;/p&gt;

&lt;h1&gt;
  
  
  Create NeonDB branch
&lt;/h1&gt;

&lt;p&gt;When a pull request is made to the main branch, this GitHub Action will fire and do a number of steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a development branch of the NeonDB database.

&lt;ul&gt;
&lt;li&gt;Grab the host and password of this new DB.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Check out the GitHub Code.&lt;/li&gt;
&lt;li&gt;Use a sed command to replace new_host &amp;amp; new_password placeholders in the .do/app.yaml file with the variables extracted in step 1a.
&lt;/li&gt;
&lt;li&gt;Install the Digital Ocean CLI.&lt;/li&gt;
&lt;li&gt;Update the App Spec with the new yaml file.&lt;/li&gt;
&lt;li&gt;Initiate a Digital Ocean deployment.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Create Neon Branch and deploy dev to DO&lt;/span&gt;
    &lt;span class="na"&gt;run-name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Create a Neon Branch 🚀&lt;/span&gt;
    &lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;types&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;opened&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
        &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;main&lt;/span&gt;
    &lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;Create-Neon-Branch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
        &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&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;Verify NEON API Key presence&lt;/span&gt;
            &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
              &lt;span class="s"&gt;if [ -z "${{ secrets.NEON_API_KEY }}" ]; then&lt;/span&gt;
                &lt;span class="s"&gt;echo "NEON_API_KEY is empty"&lt;/span&gt;
              &lt;span class="s"&gt;else&lt;/span&gt;
                &lt;span class="s"&gt;echo "NEON_API_KEY is set"&lt;/span&gt;
              &lt;span class="s"&gt;fi&lt;/span&gt;
          &lt;span class="pi"&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;Create Neon Branch&lt;/span&gt;
            &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;create-branch&lt;/span&gt;
            &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;neondatabase/create-branch-action@v5&lt;/span&gt;
            &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;project_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;orange-violet-68318343"&lt;/span&gt;
              &lt;span class="c1"&gt;# optional (defaults to your primary  branch)&lt;/span&gt;
              &lt;span class="na"&gt;parent&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;main"&lt;/span&gt; 
              &lt;span class="c1"&gt;# optional (defaults to neondb)&lt;/span&gt;
              &lt;span class="na"&gt;database&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;neondb"&lt;/span&gt;
              &lt;span class="na"&gt;branch_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;development"&lt;/span&gt;
              &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;neondb_owner"&lt;/span&gt;
              &lt;span class="na"&gt;api_key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;secrets.NEON_API_KEY&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;echo db_url ${{ steps.create-branch.outputs.db_url }}&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;echo host ${{ steps.create-branch.outputs.host }}&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;echo branch_id ${{ steps.create-branch.outputs.branch_id }}&lt;/span&gt;
          &lt;span class="pi"&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;Checkout code&lt;/span&gt;
            &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v2&lt;/span&gt;
          &lt;span class="pi"&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;Replace variables in YAML&lt;/span&gt;
            &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
              &lt;span class="s"&gt;sed -i 's|new_host|'"${{ steps.create-branch.outputs.host }}"'|g' .do/app.yaml&lt;/span&gt;
              &lt;span class="s"&gt;sed -i 's|new_password|'"${{ steps.create-branch.outputs.password }}"'|g' .do/app.yaml&lt;/span&gt;
          &lt;span class="pi"&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;Install doctl&lt;/span&gt;
            &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;digitalocean/action-doctl@v2&lt;/span&gt;
            &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.DO_KEY }}&lt;/span&gt;
          &lt;span class="pi"&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;Set environment variables&lt;/span&gt;
            &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
              &lt;span class="s"&gt;doctl auth init -t ${{ secrets.DO_KEY }}&lt;/span&gt;
              &lt;span class="s"&gt;# Update the app with the new specifications from neon&lt;/span&gt;
              &lt;span class="s"&gt;#  use active project id from DO url&lt;/span&gt;
              &lt;span class="s"&gt;doctl apps update 3aec3cab-fca5-4829-b5f4-1fd9d41b16a9  --spec .do/app.yaml&lt;/span&gt;
              &lt;span class="s"&gt;doctl apps create-deployment 3aec3cab-fca5-4829-b5f4-1fd9d41b16a9&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Hints on creating your action:&lt;/strong&gt; &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The NeonDB  project_id is in the URL string when you load the project in the NeonDB dashboard.&lt;/li&gt;
&lt;li&gt;The UUID for your Digital Ocean application is also found in the dashboard URL.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Save this workflow in &lt;code&gt;/.github/workflows&lt;/code&gt;.&lt;/p&gt;

&lt;h1&gt;
  
  
  Delete NeonDB branch
&lt;/h1&gt;

&lt;p&gt;Once the PR has been tested and approved, we want to destroy the NeonDB branch and revert the Digital Ocean deployment back to production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The steps in this are:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Delete the NeonDB development branch.&lt;/li&gt;
&lt;li&gt;Check out the code.&lt;/li&gt;
&lt;li&gt;Update the &lt;em&gt;default.yaml&lt;/em&gt; with our DB password from the GitHub Secrets.&lt;/li&gt;
&lt;li&gt;Update the App Spec and deploy the application at Digital Ocean.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Delete Neon Branch with GitHub Actions Demo&lt;/span&gt;
    &lt;span class="na"&gt;run-name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Delete a Neon Branch 🚀&lt;/span&gt;
    &lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;types&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;closed&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
        &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;main&lt;/span&gt;
    &lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;delete-neon-branch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
        &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&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;Delete Neon branch&lt;/span&gt;
            &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;neondatabase/delete-branch-action@v3&lt;/span&gt;
            &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;project_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;orange-violet-68318343"&lt;/span&gt;
              &lt;span class="na"&gt;branch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;development&lt;/span&gt;
              &lt;span class="na"&gt;api_key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.NEON_API_KEY }}&lt;/span&gt;
          &lt;span class="pi"&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;Checkout code&lt;/span&gt;
            &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v2&lt;/span&gt;
          &lt;span class="pi"&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;Replace variables in YAML&lt;/span&gt;
            &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
              &lt;span class="s"&gt;sed -i 's|new_password|'"${{ secrets.NEON_PW }}"'|g' .do/default.yaml&lt;/span&gt;
          &lt;span class="pi"&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;Install doctl&lt;/span&gt;
            &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;digitalocean/action-doctl@v2&lt;/span&gt;
            &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.DO_KEY }}&lt;/span&gt;
          &lt;span class="pi"&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;Set environment variables&lt;/span&gt;
            &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
              &lt;span class="s"&gt;doctl auth init -t ${{ secrets.DO_KEY }}&lt;/span&gt;
              &lt;span class="s"&gt;# Update the app with the new specifications from neon&lt;/span&gt;
              &lt;span class="s"&gt;#  use active project id from DO url&lt;/span&gt;
              &lt;span class="s"&gt;doctl apps update 3aec3cab-fca5-4829-b5f4-1fd9d41b16a9  --spec .do/default.yaml&lt;/span&gt;
              &lt;span class="s"&gt;doctl apps create-deployment 3aec3cab-fca5-4829-b5f4-1fd9d41b16a9&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Okay, that is a &lt;em&gt;lot of code.&lt;/em&gt; Don’t forget to update the DO UUIDs to match your deployment. Push this all to your repo so that we can see our automation in action.&lt;/p&gt;

&lt;p&gt;Here is the production version of the application running on Digital Ocean. I added a few extra elements for fun. The screenshot shows the mouse hover color on &lt;strong&gt;Ne&lt;/strong&gt; &lt;strong&gt;(Neon)&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnzr4i1xd9sc6k9yapjto.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnzr4i1xd9sc6k9yapjto.png" width="800" height="319"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now, let’s make some changes to the code and start a pull request.&lt;/p&gt;

&lt;p&gt;Create a dev branch for your code. Let’s change the colors in &lt;strong&gt;line 15&lt;/strong&gt; of/elements/templates/elements_list.html:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;li hx-delete="element/{{ element.id }}" hx-target="body" class="relative flex flex-col text-center p-5 rounded-md bg-[#7846a8] transition-colors hover:bg-orange-500 text-[white]"&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;This should make the boxes purple, with orange hover and white text.&lt;/p&gt;

&lt;p&gt;Push the dev branch and open a pull request.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3b792balxxqvrhttt3gh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3b792balxxqvrhttt3gh.png" width="800" height="314"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When the pull request is created, the “Create a Neon Branch” GitHub Action is called. A branch of the NeonDB is created, and the dev code is deployed to Digital Ocean.&lt;/p&gt;

&lt;p&gt;Refreshing the application, we see the colors have been updated:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcqnqicsefwa0mpt6y4vw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcqnqicsefwa0mpt6y4vw.png" width="800" height="337"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;More importantly, we can test all we want without worrying about the production database. Any changes made on the dev branch are in the development branch of NeonDB. As a part of our testing, we deleted a number of entries—only Neon is left:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fauzscgwg47mem9sgj29p.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fauzscgwg47mem9sgj29p.png" width="800" height="289"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Since we’re happy with the PR, we can approve and merge the changes. This fires up the second GitHub Action: deleting the NeonDB development branch and pushing the production build to Digital Ocean:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjb5iugxkpq6vwege1t9m.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjb5iugxkpq6vwege1t9m.png" width="800" height="313"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The new colors are in prod, and the prod database was untouched by our testing on the PR!&lt;/p&gt;

&lt;h1&gt;
  
  
  Conclusion
&lt;/h1&gt;

&lt;p&gt;In this post, we used GitHub Actions to automate creating and deleting a NeonDB database for testing pull request builds on Digital Ocean.  If you would like to look at the code, it is available on &lt;a href="https://github.com/dougsillars/django-neon-quickstart" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. You’ll just need to wire in your NeonDB and Digital Ocean credentials to get up and running.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Automating Developer Relations Metrics with Low Code RunBooks</title>
      <dc:creator>Doug Sillars</dc:creator>
      <pubDate>Mon, 24 Apr 2023 22:05:02 +0000</pubDate>
      <link>https://dev.to/unskript/automating-developer-relations-metrics-with-low-code-runbooks-5ch2</link>
      <guid>https://dev.to/unskript/automating-developer-relations-metrics-with-low-code-runbooks-5ch2</guid>
      <description>&lt;p&gt;My job at unSkript is to spread awareness and excitement around the DevOps tooling we have built.  But, I am also expected to provide reporting on various metrics around developer awareness and usage of our product. In this post, I walk through how I have automated the data collection process, so that I can spend more time creating content and building awareness.&lt;/p&gt;

&lt;p&gt;&lt;span&gt;A bit of background: at unSkript, we are building automation tools to reduce toil. In the DevOps/SRE space, toil is defined as the manual and repetitive work that needs to be done to keep everything shipshape.  If you ask me – collecting metrics from a bunch of different services (Github, Google Analytics, internal databases, Docker,….), and aggregating them in one place – that sounds like toil.  So let’s automate that away, and then I no longer have to think about it (until I decide to write a blog post about it, of course.)&lt;/span&gt;&lt;/p&gt;



&lt;h2&gt;&lt;span&gt;Collecting the Data&lt;/span&gt;&lt;/h2&gt;

&lt;p&gt;unSkript is a tool to help you build RunBooks. A RunBook is a collection of steps (we call them Actions) that complete a task.  For DevOps teams, that could be &lt;a href="https://unskript.com/security-checkup-force-aws-load-balancers-to-redirect-to-https/"&gt;auto-remediation of your load balancers&lt;/a&gt;, &lt;a href="https://unskript.com/runbook-analysis-of-k8s-logs/"&gt;running health checks on a K8s cluster&lt;/a&gt;, or even monitoring your &lt;a href="https://unskript.com/keeping-your-cloud-costs-in-check-automated-aws-cost-charts-and-alerting/"&gt;daily Cloud costs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I want to use these actions to collect a bunch of different data points, and store them all in one place. There are a few different ways that I use unSkript to collect the information:&lt;/p&gt;

&lt;h2&gt;&lt;span&gt;Built in Actions&lt;/span&gt;&lt;/h2&gt;

&lt;p&gt;&lt;span&gt;unSkript comes with hundreds of built-in Actions – simply drag &amp;amp; drop into your RunBook, configure your credentials, and you are ready to go!  When using built-in Actions – unSkript can be thought of as essentially “no-code” to set up. There are several built-in Actions in unSkript that are well suited to collecting the data that I want to collect: Daily Unique users from Google Analytics, and the Github star count.  &lt;/span&gt;&lt;/p&gt;



&lt;p&gt;&lt;span&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--zlNs234e--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://i0.wp.com/unskript.com/wp-content/uploads/2023/04/Screenshot-2023-04-24-at-17.31.00.jpg%3Fresize%3D300%252C85%26ssl%3D1" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--zlNs234e--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://i0.wp.com/unskript.com/wp-content/uploads/2023/04/Screenshot-2023-04-24-at-17.31.00.jpg%3Fresize%3D300%252C85%26ssl%3D1" alt="GA Action" width="300" height="85"&gt;&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;

&lt;p&gt;&lt;span&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--sCgbVWJy--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://i0.wp.com/unskript.com/wp-content/uploads/2023/04/Screenshot-2023-04-24-at-14.44.41.jpg%3Fresize%3D300%252C80%26ssl%3D1" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--sCgbVWJy--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://i0.wp.com/unskript.com/wp-content/uploads/2023/04/Screenshot-2023-04-24-at-14.44.41.jpg%3Fresize%3D300%252C80%26ssl%3D1" alt="GitHub Star Action" width="300" height="80"&gt;&lt;/a&gt;NOTE: Github stars as a DevRel metric can be controversial (IMO- it is useful as an indicator metric) but feel free to leave a comment below with your thoughts.)&lt;/span&gt;&lt;/p&gt;

&lt;h2&gt;&lt;span&gt;Database Queries &lt;/span&gt;&lt;/h2&gt;

&lt;p&gt;&lt;span&gt;Many of our stats are collected from Segment, and stored in a database (and that database is awesome for in depth detailed analysis).  But I want to keep all of my high level statistics and data in one table, so, I’ll use the PostgreSQL connector to extract the datapoints I’d like into my dataset:&lt;/span&gt;&lt;/p&gt;



&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--6CEHmwkA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://i0.wp.com/unskript.com/wp-content/uploads/2023/04/Screenshot-2023-04-24-at-11.53.29.jpg%3Fresize%3D300%252C232%26ssl%3D1" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--6CEHmwkA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://i0.wp.com/unskript.com/wp-content/uploads/2023/04/Screenshot-2023-04-24-at-11.53.29.jpg%3Fresize%3D300%252C232%26ssl%3D1" alt="3 SQL Actions to add data" width="300" height="232"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;These Actions are “low-code” in that once you drag &amp;amp; drop the action and make the connections, you still need to create a SQL query to grab the results.&lt;/p&gt;

&lt;h2&gt;&lt;span&gt;REST API&lt;/span&gt;&lt;/h2&gt;

&lt;p&gt;&lt;span&gt;There are still a few more data points that I’d like to pull out of other tools.  We have a REST API connector that makes this easy: set up your credentials and the headers you need – and you can create a new Action that extracts your data via API.  These are also “low-code” but do require some understanding of how to make API calls – in order to set up the credentials properly.&lt;/span&gt;&lt;/p&gt;



&lt;p&gt;&lt;span&gt;For example: Docker Hub publishes the number of times our Docker Image has been downloaded. We can collect this number Each day using the REST API Action – and adding the endpoint and headers to the Action:&lt;/span&gt;&lt;/p&gt;

&lt;p&gt;&lt;span&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--GI0NbCRr--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://i0.wp.com/unskript.com/wp-content/uploads/2023/04/Screenshot-2023-04-24-at-11.23.55.jpg%3Fresize%3D300%252C297%26ssl%3D1" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--GI0NbCRr--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://i0.wp.com/unskript.com/wp-content/uploads/2023/04/Screenshot-2023-04-24-at-11.23.55.jpg%3Fresize%3D300%252C297%26ssl%3D1" alt="" width="300" height="297"&gt;&lt;/a&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--J18tsC54--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://i0.wp.com/unskript.com/wp-content/uploads/2023/04/Screenshot-2023-04-24-at-11.25.27.jpg%3Fresize%3D300%252C99%26ssl%3D1" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--J18tsC54--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://i0.wp.com/unskript.com/wp-content/uploads/2023/04/Screenshot-2023-04-24-at-11.25.27.jpg%3Fresize%3D300%252C99%26ssl%3D1" alt="" width="300" height="99"&gt;&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;

&lt;h2&gt;&lt;span&gt;Storing &amp;amp; reporting our data&lt;/span&gt;&lt;/h2&gt;

&lt;p&gt;&lt;span&gt;Once we have collected all of the data, we can create a message and post it on Slack for the team to see:&lt;/span&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--s7yO2Xyb--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://i0.wp.com/unskript.com/wp-content/uploads/2023/04/Screenshot-2023-04-24-at-17.35.34.jpg%3Fresize%3D300%252C267%26ssl%3D1" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--s7yO2Xyb--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://i0.wp.com/unskript.com/wp-content/uploads/2023/04/Screenshot-2023-04-24-at-17.35.34.jpg%3Fresize%3D300%252C267%26ssl%3D1" alt="Slack Action" width="300" height="267"&gt;&lt;/a&gt;The message that is sent to the channel is a &lt;a href="https://realpython.com/python-f-strings/"&gt;Python f string&lt;/a&gt; with variables added.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--ou-lwH8K--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://i0.wp.com/unskript.com/wp-content/uploads/2023/04/Screenshot_2023-04-24_at_12_05_04.jpg%3Fresize%3D945%252C315%26ssl%3D1" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ou-lwH8K--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://i0.wp.com/unskript.com/wp-content/uploads/2023/04/Screenshot_2023-04-24_at_12_05_04.jpg%3Fresize%3D945%252C315%26ssl%3D1" alt="" width="800" height="267"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;span&gt;This is a fun way to update the team on a daily basis…but we also want to chart this data over time.  To accomplish this, we have a table in PostgreSQL for our stats, and we just make an INSERT using the prebuilt Postgres Action (again this is low-code as you must write the SQL INSERT command:&lt;/span&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--iaS5m6fA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://i0.wp.com/unskript.com/wp-content/uploads/2023/04/Screenshot-2023-04-24-at-17.37.49.jpg%3Fresize%3D300%252C64%26ssl%3D1" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--iaS5m6fA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://i0.wp.com/unskript.com/wp-content/uploads/2023/04/Screenshot-2023-04-24-at-17.37.49.jpg%3Fresize%3D300%252C64%26ssl%3D1" alt="" width="300" height="64"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;span&gt;The data in Postgres feeds a Grafana dashboard – allowing the team access to the latest data from our metrics – and the best part of it all is that there is no daily toil required.  And many folks on the team just go to the dashboard to get the data – the DevRel team is no longer a bottleneck!&lt;/span&gt;&lt;/p&gt;

&lt;h2&gt;&lt;span&gt;Progressive enhancement&lt;/span&gt;&lt;/h2&gt;

&lt;p&gt;&lt;span&gt;As time goes on, more questions about data will arise. &lt;/span&gt;&lt;/p&gt;

&lt;p&gt;&lt;span&gt;As an example, since the number of Actions and RunBooks in GitHub keeps increasing, I was recently asked “how many Actions do we have in GItHub today?” &lt;/span&gt;&lt;/p&gt;

&lt;p&gt;&lt;span&gt;The first few times you are asked a question like this, you can probably get away with waving your hands, and a ballpark figure… but after being asked a few times, I knew I needed a “real” answer. Reusing an existing GitHub Action, I was able to create a file in Github with the counts that I needed.  &lt;/span&gt;&lt;span&gt;By dragging a new Action into my RunBook, writing a few lines of Python code (and a small change on the Postgres insert), I was able to easily extend the current data collection to include more data.  &lt;/span&gt;&lt;/p&gt;

&lt;p&gt;&lt;span&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--x1LDTNH9--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://i0.wp.com/unskript.com/wp-content/uploads/2023/04/Screenshot-2023-04-24-at-17.43.42.jpg%3Fresize%3D300%252C134%26ssl%3D1" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--x1LDTNH9--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://i0.wp.com/unskript.com/wp-content/uploads/2023/04/Screenshot-2023-04-24-at-17.43.42.jpg%3Fresize%3D300%252C134%26ssl%3D1" alt="" width="300" height="134"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/span&gt;&lt;/p&gt;

&lt;p&gt;&lt;span&gt;Aside: We can also leverage these values to create custom badges for the Github readme, and on the website – so creating the data has been a double win!&lt;/span&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--YL2HuT7c--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://i0.wp.com/unskript.com/wp-content/uploads/2023/04/Screenshot-2023-04-24-at-17.44.33.jpg%3Fresize%3D300%252C148%26ssl%3D1" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--YL2HuT7c--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://i0.wp.com/unskript.com/wp-content/uploads/2023/04/Screenshot-2023-04-24-at-17.44.33.jpg%3Fresize%3D300%252C148%26ssl%3D1" alt="" width="300" height="148"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;&lt;span&gt;Scheduling&lt;/span&gt;&lt;/h2&gt;

&lt;p&gt;&lt;span&gt;Now that I have built a RunBook that collects all of data we need (so far…) -&amp;gt; I want to automate the execution of the RunBook.  Using unSkript’s Scheduler, I have set my RunBook to run at midnight GMT every day.  &lt;/span&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--mXByz9xz--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://i0.wp.com/unskript.com/wp-content/uploads/2023/04/Screenshot-2023-04-24-at-15.32.28.jpg%3Fresize%3D945%252C549%26ssl%3D1" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--mXByz9xz--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://i0.wp.com/unskript.com/wp-content/uploads/2023/04/Screenshot-2023-04-24-at-15.32.28.jpg%3Fresize%3D945%252C549%26ssl%3D1" alt="" width="800" height="465"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now, I have daily reports being created for the team, that require ZERO work on my part!&lt;/p&gt;

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

&lt;p&gt;&lt;span&gt;Collecting and aggregating statistics via automation frees the team to can focus on our “real” work: creating more tools and applications – and no longer spend significant time on metric collection.  At the same time, everyone has visibility into the project – showing the value of the DevRel team, without impacting their workload.&lt;/span&gt;&lt;/p&gt;


&lt;p&gt;&lt;span&gt;How does your DevRel team collect usage data?  If you’d like to give unSkript a try, check out our&lt;a href="https://us.app.unskript.io/"&gt; Free trial&lt;/a&gt;.  Join our &lt;a href="https://communityinviter.com/apps/cloud-ops-community/awesome-cloud-automation"&gt;Slack Channel&lt;/a&gt;, and I would be happy to chat with you on strategies to build your RunBook to collect your analytics data.  I’ll also be happy to share the skeleton of my RunBook to get you started!&lt;/span&gt;&lt;/p&gt;

</description>
      <category>blog</category>
      <category>intelligentautomatio</category>
      <category>leadership</category>
      <category>otherposts</category>
    </item>
    <item>
      <title>Automating the GitHub *Nudge*</title>
      <dc:creator>Doug Sillars</dc:creator>
      <pubDate>Tue, 04 Apr 2023 16:46:28 +0000</pubDate>
      <link>https://dev.to/unskript/automating-the-github-nudge-3i3k</link>
      <guid>https://dev.to/unskript/automating-the-github-nudge-3i3k</guid>
      <description>&lt;p&gt;Git is a tool of Actions.  Millions of times a day, users checkout, push, pull and merge submissions to their repositories.  The scale is staggering:over 3.5 billion &lt;a href="https://octoverse.github.com/2022/developer-community"&gt;contributions were made on GitHub in 2022&lt;/a&gt;.  That’s 227 million Pull Requests merged, and over 31 million issues closed.  However, What about the PRs and the issues that fall through the cracks and are forgotten?  Should they be just left, forgotten (like the Island of Misfit toys in Rudolf the RedNosed Reindeer?)&lt;/p&gt;

&lt;p&gt;In this post, we introduce an automated RunBook that introduces the “Github nudge.” Merriam-Webster’s &lt;a href="https://www.merriam-webster.com/dictionary/nudge"&gt;definition&lt;/a&gt; for a nudge is “to prod lightly &lt;strong&gt;: &lt;/strong&gt;urge into action.”  The GitHub nudge identifies issues and PRs that have been sitting a while, and “nudges” the assignee to take a look.  By not letting the team forget that the issues exist – they are more likely to be acted upon!&lt;/p&gt;

&lt;p&gt;We’ve defined a few Actions in unSkript’s RunBook architecture to help us along this path:&lt;/p&gt;

&lt;h2&gt;Stale Issues&lt;/h2&gt;

&lt;p&gt;When issues have been assigned to a team member, but no work is being done on them, the issue has probably “gotten lost.”  Everyone has a lot to work on, and sometimes these issues just lose priority – or get superseded by other tasks.  That does not mean that they should just be ignored – resolving the issue will improve the project.&lt;/p&gt;

&lt;p&gt;In unSkript, there is an Action to find “stale” issues: that is issues that are over a certain age.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--LmS7w-fH--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://i0.wp.com/unskript.com/wp-content/uploads/2023/04/Screenshot-2023-04-02-at-21.27.45.jpg%3Fresize%3D840%252C166%26ssl%3D1" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--LmS7w-fH--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://i0.wp.com/unskript.com/wp-content/uploads/2023/04/Screenshot-2023-04-02-at-21.27.45.jpg%3Fresize%3D840%252C166%26ssl%3D1" alt="GitHub Stale issues" width="800" height="158"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This Action takes 3 input parameters – the Github owner and repository and the threshold (in days) upon which you define an issue as stale.  In the case of our repository &lt;a href="https://github.com/unskript/Awesome-CloudOps-Automation"&gt;https://github.com/unskript/Awesome-CloudOps-Automation&lt;/a&gt;, the owner is unskript, the repo is Awesome-CloudOps-Automation, and we set the stale threshold at 14 days.  There’s no real science around that day -it felt like a good number for our team.&lt;/p&gt;

&lt;p&gt;The response is an array of issues that surpass that threshold. With this data, we can do some quick examinations:&lt;/p&gt;

&lt;h3&gt;Issues without an assignee&lt;/h3&gt;

&lt;p&gt;Issues that have not been assigned to a resource are not going to be worked on – we all have a lot on our plate already!  In the case of Awesome-CloudOps-Automation – these are all “good first issues” for those interested in contributing.  I like to scan this list once a week for changes – and to ensure that issues that &lt;strong&gt;do&lt;/strong&gt; need work are properly assigned.&lt;/p&gt;

&lt;h3&gt;Issues with an assignee&lt;/h3&gt;

&lt;p&gt;If an issue has been around for 2 weeks, and is not yet resolved – its good to check on them to see the status – and that action is being made:&lt;/p&gt;

&lt;p&gt;For example – it appears that issue 346 is assigned to me, and I am overdue with an update (oops!):&lt;/p&gt;

&lt;pre&gt;{'assignee': NamedUser(login="dougsillars"),
   'issue_number': 346,
   'title': '[Action]: GitHub Comment on an issue'}

&lt;/pre&gt;

&lt;p&gt;In this case, the PR was already merged, but not connected to the issue, so it could be closed (whew!).&lt;/p&gt;



&lt;h2&gt;Stale Pull Requests&lt;/h2&gt;

&lt;p&gt;We can do the same thing for Pull Requests:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--GYuGXQ1n--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://i0.wp.com/unskript.com/wp-content/uploads/2023/04/Screenshot-2023-04-04-at-12.14.30.jpg%3Fresize%3D578%252C168%26ssl%3D1" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--GYuGXQ1n--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://i0.wp.com/unskript.com/wp-content/uploads/2023/04/Screenshot-2023-04-04-at-12.14.30.jpg%3Fresize%3D578%252C168%26ssl%3D1" alt="Stale PR Action in unSkript" width="578" height="168"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A stale PR almost feels &lt;em&gt;worse&lt;/em&gt; than a stale Issue.  When the work is completed, and ready (perhaps nearly ready) to be integrated into the codebase – there is a immediate improvement to the software.  When. PR just languishes, the code does not improve, and sometimes further improvements are blocked   The Action above gives a list of all PRs that are over a threshold (again we use 14 days) for a given GitHub repository. For example:&lt;/p&gt;

&lt;pre&gt;{331: '3 cost optimization runbooks'}&lt;/pre&gt;



&lt;p&gt;This output does not provide anyone that I can ‘nudge’, but we have another Action that we can use:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--ibM2r1pg--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://i0.wp.com/unskript.com/wp-content/uploads/2023/04/Screenshot-2023-04-04-at-12.21.47.jpg%3Fresize%3D638%252C156%26ssl%3D1" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ibM2r1pg--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://i0.wp.com/unskript.com/wp-content/uploads/2023/04/Screenshot-2023-04-04-at-12.21.47.jpg%3Fresize%3D638%252C156%26ssl%3D1" alt="Github PR reviewer action" width="638" height="156"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To generate all of the reviewers to nudge, I first create a list of the PR numbers that I obtained from the first Action (the output was named “stalePull”:&lt;/p&gt;

&lt;p&gt;oldPRs = []&lt;br&gt;
for pr in stalePull[1]:&lt;br&gt;
oldPRs.append(list(pr.keys())[0])&lt;br&gt;
print(oldPRs)&lt;/p&gt;

&lt;p&gt;The Get Pull Request Reviewer Action takes 3 inputs – owner, repository and PR number.&lt;/p&gt;

&lt;p&gt;Using the iteration command in unSkript, we can apply the list of pull requests (oldPRs) to the &lt;em&gt;pull_request_number&lt;/em&gt; variable:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--m2cSY24c--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://i0.wp.com/unskript.com/wp-content/uploads/2023/04/Screenshot-2023-04-04-at-12.24.50.jpg%3Fresize%3D300%252C236%26ssl%3D1" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--m2cSY24c--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://i0.wp.com/unskript.com/wp-content/uploads/2023/04/Screenshot-2023-04-04-at-12.24.50.jpg%3Fresize%3D300%252C236%26ssl%3D1" alt="iterating through all the pull requests" width="300" height="236"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This will call the Action once per pull request – giving a full output of each PR, and who is left to review.&lt;/p&gt;

&lt;pre&gt;331: ['jayasimha-raghavan-unskript', 'shloka-bhalgat-unskript']&lt;/pre&gt;

&lt;p&gt;With a little Python, we can turn this around to list every user, and the PRs they should look at.&lt;/p&gt;

&lt;pre&gt;U03QU1K184X': [267, 153]&lt;/pre&gt;

&lt;p&gt;You may notice that the username is different.  This is in anticipation of the next step:&lt;/p&gt;

&lt;h2&gt;The actual “nudge”&lt;/h2&gt;

&lt;p&gt;Ok, so we know whose issues and PR reviews are overdue.  How do we alert them?  At unSkript, the internal team uses Slack.  I have created a table that compares each Github username with their Slack ID (that’s the weird variable above).&lt;/p&gt;

&lt;p&gt;With the slack ID, I can now send a message to the team channel:&lt;/p&gt;

&lt;pre&gt;If you are listed below, can you please review the Pull requests next to your name? They have been open for 14 days.

&amp;lt;@U01UG9DRR7D&amp;gt;, please review pull requests [267, 153].
&amp;lt;@U03QU1K184X&amp;gt;, please review pull requests [267, 153].

&lt;/pre&gt;

&lt;p&gt;Adding the @ in front of the username creates an “at” in Slack, so these two users have just been effectively nudged to look at their PRs. They have now been nudged in Slack to go into GitHub and take a look at the work that is being forgotten.&lt;/p&gt;

&lt;h2&gt;Automating the Nudge!&lt;/h2&gt;

&lt;p&gt;Using the Enterprise version (or the &lt;a href="https://us.app.unskript.io/"&gt;free trial)&lt;/a&gt; of unSkript, you can schedule each RunBook.  The RUnBook I created for Awesome-CloudOps-Automation runs every Wednesday morning – alerting the team that there are some issues and pull requests that have been left behind and should be resolved.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--v_gUwbP_--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://i0.wp.com/unskript.com/wp-content/uploads/2023/04/Screenshot-2023-04-04-at-12.40.41.jpg%3Fresize%3D945%252C142%26ssl%3D1" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--v_gUwbP_--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://i0.wp.com/unskript.com/wp-content/uploads/2023/04/Screenshot-2023-04-04-at-12.40.41.jpg%3Fresize%3D945%252C142%26ssl%3D1" alt="Slack Message" width="800" height="120"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;How do you keep your Github issues and Pull requests chugging along?  Do you have suggestions on how you might improve this RunBook?  We’d love to hear about it in the &lt;a href="https://communityinviter.com/apps/cloud-ops-community/awesome-cloud-automation"&gt;unSkript Slack channel&lt;/a&gt;. Interested in trying out Github nudges with your team?  All of the Actions described above are in our &lt;a href="https://github.com/unskript/Awesome-CloudOps-Automation"&gt;Open source&lt;/a&gt; (Docker instructions are in the readme), and in our &lt;a href="https://us.app.unskript.io/"&gt;free Trial&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>github</category>
      <category>intelligentautomation</category>
      <category>runbook</category>
    </item>
    <item>
      <title>Cloud Costs: Charting Daily EC2 Usage and Cost</title>
      <dc:creator>Doug Sillars</dc:creator>
      <pubDate>Wed, 22 Mar 2023 09:25:44 +0000</pubDate>
      <link>https://dev.to/unskript/cloud-costs-charting-daily-ec2-usage-and-cost-5ahp</link>
      <guid>https://dev.to/unskript/cloud-costs-charting-daily-ec2-usage-and-cost-5ahp</guid>
      <description>&lt;p&gt;When you buy a t-shirt at the store, you (generally) pay the same price for the shirt if you are an XS or a 2XL.  In the cloud, the size of the Virtual machine you select has VERY large implications in the cost.  At AWS, launching a t2.nano instance costs $.0058 an hour – or 14 cents a day, while a t3.2xlarge instance is $.4628 – or $11.11 a day.&lt;/p&gt;

&lt;p&gt;When sizing a new system, it is common to go a “bit larger” in size to ensure that the service performs well.  This has worked well for AWS, as &lt;a href="https://www.cnbc.com/2021/09/05/how-amazon-web-services-makes-money-estimated-margins-by-service.html"&gt;over 50% of their revenue&lt;/a&gt; comes from EC2 instances. In a &lt;a href="https://unskript.com/keeping-your-cloud-costs-in-check-automated-aws-cost-charts-and-alerting/"&gt;recent post&lt;/a&gt;, we built an automated RunBook to examine our daily spend for each AWS product, and EC2 (the green line) is far and away our biggest cost center:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--7EmHDpUV--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://i0.wp.com/unskript.com/wp-content/uploads/2023/03/Screenshot-2023-03-20-at-11.03.59.jpg%3Fresize%3D300%252C153%26ssl%3D1" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--7EmHDpUV--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://i0.wp.com/unskript.com/wp-content/uploads/2023/03/Screenshot-2023-03-20-at-11.03.59.jpg%3Fresize%3D300%252C153%26ssl%3D1" alt="AWS costs over the last 7 days" width="300" height="153"&gt;&lt;/a&gt;&lt;/p&gt;



&lt;p&gt;When looking at your highest cost center – you may wish to drill deeper into what these expenses are.  In this post, we’ll break down our daily EC2 spend by the types of instances that are running.&lt;/p&gt;

&lt;h2&gt;Creating the RunBook&lt;/h2&gt;

&lt;p&gt;In our last post, we built a RunBook that used the AWS Cost and Usage report to break down unSkript’s costs product. By running the report daily, we could chart our daily cost spend per product, and if the change day over day exceeded a threshold, we could send an alert.&lt;/p&gt;

&lt;p&gt;To study our daily EC2 usage and spend, we will use the same Cost and Usage report, and the RunBook is essentially the same – just with a different SQL query into the table.  Rather than recreate the RunBook, we will simply Duplicate the RunBook in the unSkript UI, and save it with a new name:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--gkCE3_F6--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://i0.wp.com/unskript.com/wp-content/uploads/2023/03/Screenshot-2023-03-20-at-14.21.11.jpg%3Fresize%3D300%252C218%26ssl%3D1" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--gkCE3_F6--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://i0.wp.com/unskript.com/wp-content/uploads/2023/03/Screenshot-2023-03-20-at-14.21.11.jpg%3Fresize%3D300%252C218%26ssl%3D1" alt="Duplicating a RunBook" width="300" height="218"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To run a AWS Redshift query, we need a SecretArn, the SQL Query, AWS Region and Redshift Cluster and database details.&lt;/p&gt;

&lt;p&gt;The RunBook generates these all for us, but we need to update the SqlQuery variable to query EC2 data:&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sqlQuery = f"SELECT date_part(day, cast(lineitem_usagestartdate as date)) as day, product_instancetype,SUM(lineitem_usageamount)::numeric(37, 4) AS usage_hours, SUM((lineitem_unblendedcost)::numeric(37,4)) AS usage_cost FROM {tableName} WHERE length(lineitem_usagestartdate)&amp;amp;gt;8 AND product_productfamily = 'Compute Instance' AND pricing_unit IN ('Hours', 'Hrs') GROUP BY day, product_instancetype ORDER BY 1 DESC, 3 DESC, 2 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The next few steps of this RunBook is unchanged from the Product Cost RunBook:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--2Vom7xEJ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://i0.wp.com/unskript.com/wp-content/uploads/2023/03/Screenshot_2023-03-20_at_14_27_50.jpg%3Fresize%3D945%252C546%26ssl%3D1" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--2Vom7xEJ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://i0.wp.com/unskript.com/wp-content/uploads/2023/03/Screenshot_2023-03-20_at_14_27_50.jpg%3Fresize%3D945%252C546%26ssl%3D1" alt="" width="800" height="462"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We take the Ann and the query – and run the query – then pull the results into the RunBook.&lt;/p&gt;

&lt;p&gt;We must now change the chart – since the inputs are different. Our Columns are now “product_instancetype” and the y values are “usage_cost.”&lt;/p&gt;

&lt;p&gt;Charting this data helps us see which instance types are costing the most money:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--7IvEYslu--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://i0.wp.com/unskript.com/wp-content/uploads/2023/03/Screenshot-2023-03-20-at-14.52.44.jpg%3Fresize%3D945%252C460%26ssl%3D1" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--7IvEYslu--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://i0.wp.com/unskript.com/wp-content/uploads/2023/03/Screenshot-2023-03-20-at-14.52.44.jpg%3Fresize%3D945%252C460%26ssl%3D1" alt="Daily EC2 costs by size" width="800" height="389"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Our daily costs are very much proportional to size, with the 2xLarge and Large instances accounting for ~$20/day.  It is interesting to note that earlier this month, our t2.large went down at the same time our t2.micro grew – this could possibly be interpreted as resizing of an EC2 instance that was too large.&lt;/p&gt;



&lt;p&gt;Finally, we can build alerts to tell us if any of our costs jump by over $1 a day, or 10%.  If the costs make a jump we can send the chart EC2 usage to Slack using the &lt;em&gt;Send Image To Slack&lt;/em&gt; Action (coming this week to our Open Source).  We also send this image every Monday for a historical record:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--9odRHyh5--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://i0.wp.com/unskript.com/wp-content/uploads/2023/03/Screenshot-2023-03-20-at-15.16.00.jpg%3Fresize%3D874%252C546%26ssl%3D1" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--9odRHyh5--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://i0.wp.com/unskript.com/wp-content/uploads/2023/03/Screenshot-2023-03-20-at-15.16.00.jpg%3Fresize%3D874%252C546%26ssl%3D1" alt="Slack message with the chart" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;



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

&lt;p&gt;When it comes to FinOps – the team in charge of understanding and accounting for your cloud bill, one of the most critical tools to have is observability into your daily spend. If there is a large change – that might be ok – but it is good to know in advance, and double check that the increase is valid – thereby potentially avoiding a large bill at the end of the month.&lt;/p&gt;



&lt;p&gt;In our &lt;a href="https://unskript.com/keeping-your-cloud-costs-in-check-automated-aws-cost-charts-and-alerting/"&gt;previous post&lt;/a&gt;, we looked at all AWS products for daily cost spend, and in this post, we dug deeper to better understand our EC2 spend – the largest percentage of our AWS bill.  Are you interested in trying out our alerting in unSkript?  Try out our Open Source Docker build (and give us a star!), or sign up for a &lt;a href="https://us.app.unskript.io/"&gt;free cloud trial&lt;/a&gt;.  Is there a segment of YOUR AWS bill that you’d like to investigate with us?  Reach out in our &lt;a href="https://communityinviter.com/apps/cloud-ops-community/awesome-cloud-automation"&gt;Slack channel&lt;/a&gt;, and we’d be happy to help you create a RunBook for your use case!&lt;/p&gt;

</description>
      <category>blog</category>
      <category>cloudcosts</category>
    </item>
    <item>
      <title>Keeping your Cloud Costs in Check: Automated AWS Cost Charts and Alerting</title>
      <dc:creator>Doug Sillars</dc:creator>
      <pubDate>Mon, 20 Mar 2023 16:03:51 +0000</pubDate>
      <link>https://dev.to/unskript/keeping-your-cloud-costs-in-check-automated-aws-cost-charts-and-alerting-12p5</link>
      <guid>https://dev.to/unskript/keeping-your-cloud-costs-in-check-automated-aws-cost-charts-and-alerting-12p5</guid>
      <description>&lt;p&gt;Building and deploying infrastructure in the cloud is (by design) a very simple process. If a team is not being careful in their deployments, it can also become an &lt;strong&gt;expensive&lt;/strong&gt; process. The interplay between finance teams and cloud teams has led to a new job function – FinOps. What is FinOps? This team (or team member) works with developer teams to understand cloud needs, negotiates better prices with cloud operators, and can help translate cloud expenses and needs to the finance team.&lt;/p&gt;

&lt;p&gt;However, many companies working in the could don’t have the luxury of allocating a team member to understanding cloud costs, and it is left to the dev teams to do their best to mitigate surprise bill.  Without a full-time FInOps professional, teams will need tooling to help them better understand and control their cloud bills.&lt;/p&gt;

&lt;p&gt;The FinOps Institute has defined 6 domains for a FinOps tram:&lt;/p&gt;



&lt;ul&gt;
&lt;li&gt;Understanding Cloud Usage and Cost&lt;/li&gt;
&lt;li&gt;Performance Tracking and Benchmarking&lt;/li&gt;
&lt;li&gt;Real Time Decision Making&lt;/li&gt;
&lt;li&gt;Cloud Rate Optimization&lt;/li&gt;
&lt;li&gt;Cloud Usage Optimization&lt;/li&gt;
&lt;li&gt;Organizational Alignment&lt;/li&gt;
&lt;/ul&gt;



&lt;p&gt;In this post, we’ll describe a series of automated RunBooks that check the first three boxes (and help inform several others).  What we’ll do is build automated reporting around the AWS Cost and Usage Report (CUR).&lt;/p&gt;



&lt;h2&gt;&lt;strong&gt;The AWS CUR&lt;/strong&gt;&lt;/h2&gt;

&lt;p&gt;AWS’ Cost and Usage Report is a SQL report that breaks down your AWS spend in many different ways, and at a selected interval (hourly, daily, monthly).  When generating a CUR report, a SQL file is placed into a S3 bucket at regular intervals.  To set up a CUR report for your AWS Account, the AWS Documentation has a very &lt;a href="https://docs.aws.amazon.com/cur/latest/userguide/cur-create.html"&gt;nice tutorial&lt;/a&gt;.  In this post, our CUR is updated daily, but for larger projects, you may want hourly granularity.&lt;/p&gt;

&lt;p&gt;We set our CUR report to be sent into AWS Redshift.  However, the daily updates are only added to the S3 file.  To update our Redshift table, we must regularly take the file in SQL and update the database in Redshift.&lt;/p&gt;

&lt;p&gt;We accomplish this by building a RunBook using a few new Actions:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--2gxuQtvE--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://i0.wp.com/unskript.com/wp-content/uploads/2023/03/Screenshot-2023-03-15-at-23.23.20.jpg%3Fresize%3D694%252C654%26ssl%3D1" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--2gxuQtvE--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://i0.wp.com/unskript.com/wp-content/uploads/2023/03/Screenshot-2023-03-15-at-23.23.20.jpg%3Fresize%3D694%252C654%26ssl%3D1" alt="RunBook to update table in Redshift" width="694" height="654"&gt;&lt;/a&gt;&lt;/p&gt;



&lt;p&gt;When Making a RedShift Query, we need to know the Secret Manager ARN, the AWS Region, and the Redshift Cluster/database.&lt;/p&gt;

&lt;p&gt;To get the Secret Arn, we use the &lt;em&gt;AWS GET Secrets Manager ARNAction.  &lt;/em&gt;This takes a secret name, and provides the ARN.  (This does require Secrets Manager permission in your IAM credential.)&lt;/p&gt;

&lt;p&gt;We then Create two SQL queries programatically.  The table in Redshift is named &lt;em&gt;awsbilling202303&lt;/em&gt; (since it is currently March 2023). To ensure that the table name is always correct, we generate the query programmatically, so that the table name always has the format &lt;em&gt;awsbilling&amp;lt;year&amp;gt;&amp;lt;month&amp;gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Next, we perform two SQL commands:&lt;/p&gt;

&lt;p&gt;First, we TRUNCATE the table. This removes all of the rows, but keeps the columns:&lt;/p&gt;

&lt;pre&gt;truncate table awsbilling202303&lt;/pre&gt;

&lt;p&gt;Next, we COPY the rows from the SQL table in AWS:&lt;/p&gt;

&lt;pre&gt;copy awsbilling202303 from 's3://unskript-billing-doug/all/unskript-billing-doug/20230301-20230401/unskript-billing-doug-RedshiftManifest.json' credentials 'aws_iam_role=arn:aws:iam::&amp;lt;arn name&amp;gt;' region 'us-west-2' GZIP CSV IGNOREHEADER 1 TIMEFORMAT 'auto' manifest;&lt;/pre&gt;

&lt;p&gt;This query is provided in your S3 bucket.&lt;/p&gt;

&lt;p&gt;With the RunBook created, we can schedule this RunBook to run daily, ensuring that the AWS table is always up to date.&lt;/p&gt;

&lt;p&gt;NOTE:  I am not a database expert.  This is the “I have a hammer, so everything must be a nail” approach to updating the database.  There is probably a more nuanced query that could be run.&lt;/p&gt;

&lt;h2&gt;Building Charts and Alerts&lt;/h2&gt;

&lt;p&gt;Now that the data is being populated into Redshift daily, we can begin exploring our data.  In this second RunBook, we are going to extract the daily spent for each AWS service, plot the data, and create an alert for large changes in costs.&lt;/p&gt;

&lt;p&gt;Our new RunBook begins the same way as our first RunBook – generating a SQL query and executing it at RedShift.  This time we are querying Redshift for usage costs for every AWS product:&lt;/p&gt;

&lt;pre&gt;select lineitem_productcode, 
        date_part(day, cast(lineitem_usagestartdate as date)) as day, 
        SUM((lineitem_unblendedcost)::numeric(37,4)) as cost 
from awsbilling202303 
group by lineitem_productcode, day 
order by cost desc;

&lt;/pre&gt;

&lt;p&gt;This query is then placed into a dataframe. Using this data, we can create a chart of our daily spend by AWS product:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Orj5jUH8--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://i0.wp.com/unskript.com/wp-content/uploads/2023/03/Screenshot-2023-03-20-at-11.03.39.jpg%3Fresize%3D945%252C496%26ssl%3D1" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Orj5jUH8--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://i0.wp.com/unskript.com/wp-content/uploads/2023/03/Screenshot-2023-03-20-at-11.03.39.jpg%3Fresize%3D945%252C496%26ssl%3D1" alt="AWS Product Cost by day" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here is the same chart – just looking at the last seven days:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--BWCrLf1L--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://i0.wp.com/unskript.com/wp-content/uploads/2023/03/Screenshot-2023-03-20-at-11.03.59.jpg%3Fresize%3D945%252C482%26ssl%3D1" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--BWCrLf1L--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://i0.wp.com/unskript.com/wp-content/uploads/2023/03/Screenshot-2023-03-20-at-11.03.59.jpg%3Fresize%3D945%252C482%26ssl%3D1" alt="AWS costs over the last 7 days" width="800" height="408"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If we look back at the FinOps Institute bullet list, we are beginning the FinOps Institute bullets: &lt;em&gt;Understanding Cloud Usage and Cost&lt;/em&gt;, as well as &lt;em&gt;Tracking our Performance&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;If we automate this RunBook to run daily (after the table is updated), we can generate this regular plot of our AWS Cloud Spend.  In general, a daily chart with no change is not of great interest.  But – if we can build an alert around our Cloud usage that finds significant increases in day to day cost – we can attach this chart to make the cost jump easy to identify.&lt;/p&gt;

&lt;h2&gt;Building an Alert&lt;/h2&gt;

&lt;p&gt;Every organization will have different thresholds for alerting. In the following code, we examine the 2 days previous (March 18 and 19) and look for increases of over 5%.  Since many of the services in this chart are at very low spend rates, we add the additional filter that the change must be over $1. Looping over each service with the following:&lt;/p&gt;

&lt;pre&gt;if abs(todayCost-yesterdayCost) &amp;gt;1: 
   if delta &amp;gt;.05:
       #print( instance, delta,dfpivot.at[today, instance], dfpivot.at[yesterday, instance])
       bigchange[instance] = {"delta":delta, "todayCost":todayCost,"yesterdayCost":yesterdayCost}
       alertText = '&lt;a class="mentioned-user" href="https://dev.to/here"&gt;@here&lt;/a&gt; There has been a large change in AWS Costs'
       alert = True
if date.today().weekday() == 0:
   alertText = 'Today is Monday, Here is the last week of AWS Costs'
   alert = True&lt;/pre&gt;

&lt;p&gt;If any of the changes in cost trigger this alert, we send an image on Slack with a ‘&lt;a class="mentioned-user" href="https://dev.to/here"&gt;@here&lt;/a&gt;, there’s been a large change in AWS Spending” message.  We also send the chart every day on Monday., so that there is a visual history of AWS spending.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--DtPe_Aon--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://i0.wp.com/unskript.com/wp-content/uploads/2023/03/Screenshot-2023-03-20-at-11.32.19.jpg%3Fresize%3D945%252C510%26ssl%3D1" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--DtPe_Aon--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://i0.wp.com/unskript.com/wp-content/uploads/2023/03/Screenshot-2023-03-20-at-11.32.19.jpg%3Fresize%3D945%252C510%26ssl%3D1" alt="Slack message with chart of AWS spending" width="800" height="432"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When beginning to study and alert on your Cloud Spending it is important to start with simple reports and altering. This use case of daily spend by product, is a great start into our journey into the FinOps Institute’s next bullet points: &lt;em&gt;Real Time Monitoring&lt;/em&gt; and &lt;em&gt;Cloud Usage Optimization&lt;/em&gt;.&lt;/p&gt;



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

&lt;p&gt;Many companies feel the monthly dread of “how big will my Cloud bill be &lt;strong&gt;this&lt;/strong&gt; month?” By charting and alerting on your daily spend across all Cloud products, your team is less likely to be surprised by the beill at the end of the month. This data can also be used mitigate large changes that often result in bill “surprises.”&lt;/p&gt;

&lt;p&gt;Using unSkript and the AWS Cost and Usage report, you can begin (or continue) your FinOps journey by better understanding where your costs are coming from and how they are changing day to day.  Watch our blog for more posts on Cloud CostOps and how you can monitor your AWS bill.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>blog</category>
      <category>cloudcosts</category>
      <category>cloudops</category>
    </item>
    <item>
      <title>AWS Service Quotas: Discovering Where you Stand</title>
      <dc:creator>Doug Sillars</dc:creator>
      <pubDate>Fri, 17 Feb 2023 19:31:01 +0000</pubDate>
      <link>https://dev.to/unskript/aws-service-quotas-discovering-where-you-stand-4kc2</link>
      <guid>https://dev.to/unskript/aws-service-quotas-discovering-where-you-stand-4kc2</guid>
      <description>&lt;p&gt;We’ve written a few posts in the last week about AWS Service Quotas.  These are restrictions on services that are set by AWS (but can often be increased).&lt;/p&gt;

&lt;p&gt;If our &lt;a href="https://unskript.com/aws-service-quotas-what-are-they-and-how-can-i-increase-them/" rel="noopener noreferrer"&gt;first post, we looked at New Actions in unSkript&lt;/a&gt; that can be used to determine quota values and request a quota increase.  In this post, we’ll take the Actions a step further, and build an Action that compares the AWS quota to actual usage – generating an alert when a threshold is met.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;

&lt;p&gt;To begin, we will need to consider how the Action will work.  For any given service, we’ll need to query AWS at least twice:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Get the Quota Limit.&lt;/li&gt;
&lt;li&gt;Determine the usage of a service.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Every query requires one call to complete Step 1.  However, Step two can require many queries to complete the usage query.  In the simplest case, we can do just one query:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Example: _Client VPN Endpoints per Region. _ If we query AWS for the list of endpoints in a region, we can simply get the length of the response to know how many endpoints exist.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;However, there are times where there will be multiple queries:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Example: &lt;em&gt;Routes per Client VPN Endpoint&lt;/em&gt;. In the first query, we get the list of VPN endpoints.  In step 2, we must query every VPN endpoint to get the count of Routes.  If there are 4 VPN endpoints, there will be a total of 5 calls made (On call to get the list of 4 VPN endpoints, and then one call to each of the four endpoints).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;To account for these two options, we create an input Dictionary.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Simple, one pass Dictionary
&lt;/h2&gt;

&lt;p&gt;For the &lt;em&gt;Describe AMIs&lt;/em&gt; call (only one Usage query is required), the Dict 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;{'QuotaName':'AMIs','ServiceCode':'ec2','QuotaCode': 'L-B665C33B',
'ApiName': 'describe_images', 'ApiFilter' : '[]','ApiParam': 'Images',
'initialQuery': ''},

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

&lt;/div&gt;



&lt;p&gt;To get the Quota, we need the ServiceCode and the QuotaCode (If you need to obtain these variables, you can use the unSkript Action, or you can refer to the table in the &lt;a href="https://docs.unskript.com/unskript-product-documentation/lists/test" rel="noopener noreferrer"&gt;unSkript Docs)&lt;/a&gt;.  The one usage API call will be made to the &lt;em&gt;describe_images&lt;/em&gt; endpoint, and retrieve a list of &lt;em&gt;Images&lt;/em&gt;.  Counting this length gives us our usage.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Two Pass Dictionary
&lt;/h2&gt;

&lt;p&gt;To determine the &lt;em&gt;Attachments per transit gateway&lt;/em&gt;, we must again get the quota from the Service Code and Quota Code.  To get the count of attachments per transit gateway, we us the &lt;em&gt;initalQuery&lt;/em&gt; array to make a first query.&lt;/p&gt;

&lt;p&gt;The first query probes the &lt;em&gt;describe_transit_gateways&lt;/em&gt; endpoint, to give a list of &lt;em&gt;TransitGateways. _ In the second set of calls, we call the _describe_transit_gateway_attachments&lt;/em&gt; endpoint for each transit gateway. The filter has a string VARIABLE that is replaced with the &lt;em&gt;TransitGatewayId&lt;/em&gt; for each gateway -ensuring that each call is made to a different transit gateway.  We can then count the length of the response to find out how many attachments are in each transit gateway.  If we have 12 transit gateways. we will have 12 usage reports.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{'QuotaName':'Attachments per transit gateway','ServiceCode':'ec2','QuotaCode': 'L-E0233F82',
'ApiName': 'describe_transit_gateway_attachments', 'ApiFilter' : '[{"Name": "transit-gateway-id","Values": ["VARIABLE"]}]',
'ApiParam': 'TransitGatewayAttachments',
'initialQuery': '["describe_transit_gateways","TransitGateways", "TransitGatewayId"]'},

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

&lt;/div&gt;



&lt;h2&gt;
  
  
  Outliers
&lt;/h2&gt;

&lt;p&gt;For most of our quota measurements, these two approaches work well.  However, with over 2600 different quotas inside AWS, not all of them fit neatly into these two buckets. For example &lt;em&gt;Multicast Network Interfaces per transit gateway&lt;/em&gt; requires 3 calls: Transit gateways -&amp;gt; Multicast Domains – &amp;gt; Domain attachments.&lt;/p&gt;

&lt;p&gt;For others, there is custom code to iterate over.  These require an extra if statement in the code to properly account for their usage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Action Format
&lt;/h2&gt;

&lt;p&gt;We can differentiate between the two types of query by looking at the ‘initialQuery’ parameter. If it is empty, we can do the Simple query, otherwise, do the double query (with a for loop that queries each initial result).  For outliers, we can add specific code inside the if/else:&lt;/p&gt;

&lt;p&gt;(this is simplified a bit from what actually runs):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;for i in table: 
    #get quota 
    sq = sqClient.get_service_quota(ServiceCode=i.get('ServiceCode'),QuotaCode=i.get('QuotaCode')) 
    quotaValue =sq['Quota']['Value'] 

    #get usage     
    if i.get('initialQuery') = '': 
       res = aws_get_paginator(ec2Client, i.get('ApiName'), i.get('ApiParam'), Filters=filterList) 
       count = len(res) 
       percentage = count/quotaValue 
       combinedData = {'Quota Name': i.get('QuotaName'), 'Limit':quotaValue, 'used': count, 'percentage':percentage} 
       result.append( combinedData) 
       print(combinedData) else: res = aws_get_paginator(ec2Client, i.get('ApiName'), i.get('ApiParam'), Filters=filterList) 
       for j in res: #build the filter query with some simple substitutions res2 = aws_get_paginator(ec2Client, i.get('ApiName'), i.get('ApiParam'), Filters=filterList) count = len(res2) percentage = count/quotaValue objectResult = {j[initialQueryFilter] : count} quotaName = f"{i.get('QuotaName')} for {j[initialQueryFilter]}" combinedData = {'Quota Name': quotaName, 'Limit':quotaValue, 'used': count, 'percentage':percentage} result.append(combinedData)

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

&lt;/div&gt;



&lt;h2&gt;
  
  
  Action Output
&lt;/h2&gt;

&lt;p&gt;Once all of the values have been collected, the percentage utilized is compared to the warning percentage input. If the utilization is over the requested percentage, the Service data will be added to the output of the Action. With this information, the SRE responsible can decide the correct Action to take – either prune away some usage, or request an increase from AWS.&lt;/p&gt;

&lt;p&gt;For example, testing all VPC Service quotas with a earning of 50% utilization gives the following data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{'Instances': [{'Limit': 20.0,
                'Quota Name': 'VPCs Per Region',
                'percentage': 0.65,
                'used': 13},
               {'Limit': 20.0,
                'Quota Name': 'Internet gateways per Region',
                'percentage': 0.6,
                'used': 12},
               {'Limit': 5.0,
                'Quota Name': 'NAT gateways per Availability Zone',
                'percentage': 0.8,
                'used': 4},
               {'Limit': 50.0,
                'Quota Name': 'Routes per route table',
                'percentage': 0.5,
                'used': 25},
               {'Limit': 20.0,
                'Quota Name': 'Rules per network ACL',
                'percentage': 0.65,
                'used': 13}]}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Availability Today
&lt;/h2&gt;

&lt;p&gt;As we publish this article, we have 2 Actions heading into unSkript:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A general &lt;em&gt;AWS_ServiceQuota Compare&lt;/em&gt; Action that has the basic framework described above. This will likely require customization for each Quota you wish to test against.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;AWS VPC Service Quota Warning&lt;/em&gt;. This Action takes all of the VPC service quotas (as of February 2023) and tests them against your infrastructure.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Coming Soon:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;em&gt;AWS EC2 Service Quota Warning&lt;/em&gt;. This Action will test your infrastructure against all EC2 Service Quotas, and warn you if you are approaching the quota threshold.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;WE’re really excited to see how people use these Service Quota alerts in their infrastructure.  If you have questions – feel free to reach out in our &lt;a href="https://communityinviter.com/apps/cloud-ops-community/awesome-cloud-automation" rel="noopener noreferrer"&gt;Slack Community&lt;/a&gt;.  If you haven’t tried unSkript – try our &lt;a href="https://github.com/unskript/Awesome-CloudOps-Automation" rel="noopener noreferrer"&gt;OSS Docker Container&lt;/a&gt;, or use our &lt;a href="https://us.app.unskript.io/" rel="noopener noreferrer"&gt;free trial online!&lt;/a&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>intelligentautomatio</category>
      <category>otherposts</category>
      <category>servicehealth</category>
    </item>
    <item>
      <title>AWS Service Quotas, or AWS has a LOT of Services!</title>
      <dc:creator>Doug Sillars</dc:creator>
      <pubDate>Wed, 15 Feb 2023 18:03:21 +0000</pubDate>
      <link>https://dev.to/unskript/aws-service-quotas-or-aws-has-a-lot-of-services-cp4</link>
      <guid>https://dev.to/unskript/aws-service-quotas-or-aws-has-a-lot-of-services-cp4</guid>
      <description>&lt;p&gt;In our &lt;a href="https://unskript.com/aws-service-quotas-what-are-they-and-how-can-i-increase-them/" rel="noopener noreferrer"&gt;recent post&lt;/a&gt;, we unveiled unSkript Actions that can query AWS Service Quotas.  Service quotas are limits imposed by AWS on how many times a certain AWS feature can be used.  Most of them are adjustable with a simple request, and our post showed how to determine your Service Quota values, AND request an increase using unSkript.&lt;/p&gt;

&lt;p&gt;In this post, I thought it might be fun to dig into AWS Service Quotas a bit deeper, and get s general idea of how Service Quotas fit into the AWS landscape.&lt;/p&gt;

&lt;p&gt;To get the Service Quota value, you need to know the Service Name, and the Quota Code.  But how do you get these values?&lt;/p&gt;

&lt;h2&gt;
  
  
  AWS Service Names
&lt;/h2&gt;

&lt;p&gt;We can get all of the AWS Service Names using the Service Node endpoint. Running this call, we find that there are 221 named services in AWS (as of Feb 15, 2023).  AWS gets a &lt;a href="https://ben11kehoe.medium.com/dear-aws-we-need-to-talk-about-service-naming-d33ea68027d8" rel="noopener noreferrer"&gt;lot&lt;/a&gt; &lt;a href="https://expeditedsecurity.com/aws-in-plain-english/" rel="noopener noreferrer"&gt;of&lt;/a&gt; &lt;a href="https://twitter.com/QuinnyPig/status/1070451608050315264" rel="noopener noreferrer"&gt;flak&lt;/a&gt; for their naming conventions, but with so many services, if course some are going to have sup-optimal names. Lucky for us, we’ll be using the ServiceCode, and not the Service Name, so “_ &lt;strong&gt;AWS Systems Manager Incident Manager Contacts&lt;/strong&gt; &lt;em&gt;” is simply “ **_ssm-contacts&lt;/em&gt;** ” and “&lt;em&gt;&lt;strong&gt;AWS IAM Identity Center (successor to AWS Single Sign-On)&lt;/strong&gt;&lt;/em&gt;” is just “_ &lt;strong&gt;sso.&lt;/strong&gt; _”&lt;/p&gt;

&lt;h2&gt;
  
  
  AWS Service Quotas
&lt;/h2&gt;

&lt;p&gt;Next, we can run these 221 named services against the “List Service Quotas” endpoint to get all of the Service Quotas for all of the Services.  Only 113 of AWS Services (51%) have features that have a service quota.  Even with just half of services having quotas – there are a _ &lt;strong&gt;LOT&lt;/strong&gt; _ of preset quotas in AWS:  2,629 of them in fact! (Feb 15,2023)&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Service Code&lt;/th&gt;
&lt;th&gt;Count of  Quota Name&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;sagemaker&lt;/td&gt;
&lt;td&gt;702&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ec2&lt;/td&gt;
&lt;td&gt;131&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;iotwireless&lt;/td&gt;
&lt;td&gt;102&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;kinesisvideo&lt;/td&gt;
&lt;td&gt;82&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;rekognition&lt;/td&gt;
&lt;td&gt;80&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;personalize&lt;/td&gt;
&lt;td&gt;66&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cases&lt;/td&gt;
&lt;td&gt;65&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;braket&lt;/td&gt;
&lt;td&gt;62&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;elasticmapreduce&lt;/td&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;geo&lt;/td&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;comprehend&lt;/td&gt;
&lt;td&gt;56&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;kms&lt;/td&gt;
&lt;td&gt;53&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;lookoutmetrics&lt;/td&gt;
&lt;td&gt;44&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;sns&lt;/td&gt;
&lt;td&gt;44&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;logs&lt;/td&gt;
&lt;td&gt;41&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;apigateway&lt;/td&gt;
&lt;td&gt;38&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;iotcore&lt;/td&gt;
&lt;td&gt;37&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;chime&lt;/td&gt;
&lt;td&gt;36&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;forecast&lt;/td&gt;
&lt;td&gt;33&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ebs&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;glue&lt;/td&gt;
&lt;td&gt;28&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;acm-pca&lt;/td&gt;
&lt;td&gt;26&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fsx&lt;/td&gt;
&lt;td&gt;26&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;robomaker&lt;/td&gt;
&lt;td&gt;24&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;rds&lt;/td&gt;
&lt;td&gt;24&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;elasticloadbalancing&lt;/td&gt;
&lt;td&gt;22&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;dataexchange&lt;/td&gt;
&lt;td&gt;22&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;mediapackage&lt;/td&gt;
&lt;td&gt;22&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;monitoring&lt;/td&gt;
&lt;td&gt;22&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cognito-idp&lt;/td&gt;
&lt;td&gt;21&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;elasticfilesystem&lt;/td&gt;
&lt;td&gt;21&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;vpc&lt;/td&gt;
&lt;td&gt;21&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fis&lt;/td&gt;
&lt;td&gt;19&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;servicecatalog&lt;/td&gt;
&lt;td&gt;17&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;omics&lt;/td&gt;
&lt;td&gt;17&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ssm-contacts&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;application-autoscaling&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ssm&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cassandra&lt;/td&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;frauddetector&lt;/td&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;imagebuilder&lt;/td&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;servicequotas&lt;/td&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;workspaces&lt;/td&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;textract&lt;/td&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;lex&lt;/td&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;events&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;amplify&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;appmesh&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;route53resolver&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;iottwinmaker&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;databrew&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;medialive&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;rolesanywhere&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;mgn&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;access-analyzer&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;eks&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;groundstation&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;nimble&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;dms&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;dynamodb&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;workspaces-web&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ecr&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ivschat&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;profile&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;batch&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;proton&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;mediastore&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;drs&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;appconfig&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pinpoint&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;schemas&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cloudformation&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;athena&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;m2&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ivs&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ec2-ipam&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;es&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;refactor-spaces&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;kafka&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;resiliencehub&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;app-integrations&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;network-firewall&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;resource-explorer-2&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AWSCloudMap&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;qldb&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ssm-sap&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;mediaconnect&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;auditmanager&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;sms&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;lambda&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;guardduty&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ram&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ses&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;autoscaling&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fargate&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cloudhsm&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;outposts&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;airflow&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;dlm&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;license-manager&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ssm-guiconnect&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;connect-campaigns&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;connect&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;macie2&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;iotanalytics&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;emr-serverless&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;codeguru-profiler&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;autoscaling-plans&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;firehose&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;codeguru-reviewer&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;kinesis&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;grafana&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ec2fastlaunch&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;As Machine Learning can be a very expensive process, it is no surprise that Amazon SageMaker leads the pack with over 700 service quotas.  2nd in line is an oldie but a goodie, Amazon’s EC2 (debuted in 2006!) with 131 quotas.&lt;/p&gt;

&lt;h2&gt;
  
  
  What do we know about quotas?
&lt;/h2&gt;

&lt;p&gt;The longest quota name belongs to Rekognition, and it is quote a mouthful:&lt;/p&gt;

&lt;p&gt;_ &lt;strong&gt;Transactions per second per account for the Amazon Rekognition Image personal protective equipment operation DetectProtectiveEquipment&lt;/strong&gt;. _with a quota of 5.  That’s a lot of words to say that the service can scan 5 frames per second to identify a helmet, face mask or gloves on anyone in the picture.  This quota can be adjusted, if desired.&lt;/p&gt;

&lt;p&gt;The largest Quota is &lt;strong&gt;ElasticFileSystem’s file size&lt;/strong&gt; , weighing in at 52673613135872 bytes.  (Which, if I did my math correctly, is 47.9 TB).  This is a hard limit and cannot be adjusted.&lt;/p&gt;

&lt;p&gt;The second largest quota is the &lt;strong&gt;Maximum number of rows in a dataset&lt;/strong&gt; for &lt;strong&gt;Amazon Forecast,&lt;/strong&gt; with a soft limit of 3 billion rows.  You can request that this number be increased.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fi0.wp.com%2Funskript.com%2Fwp-content%2Fuploads%2F2023%2F02%2F3billion.jpeg%3Fresize%3D500%252C500%26ssl%3D1" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fi0.wp.com%2Funskript.com%2Fwp-content%2Fuploads%2F2023%2F02%2F3billion.jpeg%3Fresize%3D500%252C500%26ssl%3D1" alt="3 billion rows meme" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Quota adjustment
&lt;/h2&gt;

&lt;p&gt;Of the 2,629 quotas, 2,003 can be adjusted (76%), and 626 (24%) cannot be changed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quota Units
&lt;/h2&gt;

&lt;p&gt;Only 87 of our quotas have units (3.3%).  20 are time based, and the remaining 67 are data sizes (of varying magnitude):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Unit&lt;/th&gt;
&lt;th&gt;Count&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Millisecond&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Second&lt;/td&gt;
&lt;td&gt;18&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These vary from 200 ms ( &lt;strong&gt;API Gateway Maximum Integration Timeout&lt;/strong&gt; ) to 30 days: &lt;strong&gt;SageMaker’s Longest run time for an AutoML job from creation to termination&lt;/strong&gt;. In case you were wondering, 30 days is also 2,592,000 seconds.)&lt;/p&gt;

&lt;p&gt;When it comes to units, there’s nothing like arbitrarily multiplying by 1024 to change the units (and I see you MegaBits and your extra x8… but these are all throughput, so I’ll give that a pass).&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Unit&lt;/th&gt;
&lt;th&gt;Count&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Bytes&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kilobytes&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Megabits&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Megabytes&lt;/td&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gigabytes&lt;/td&gt;
&lt;td&gt;22&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Terabytes&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The smallest value is &lt;strong&gt;Lookout Metrics Value Length&lt;/strong&gt; at 40 Bytes, and the largest is &lt;strong&gt;RDS Total storage for all DB instances&lt;/strong&gt; at 100,000 GB (or 97 TB).&lt;/p&gt;

&lt;p&gt;The winner for the oddest size measurement goes to &lt;strong&gt;&lt;em&gt;Elasticfilesystem’s Throughput per NFC Client&lt;/em&gt;&lt;/strong&gt; at 524.288 MegaBytes.&lt;/p&gt;

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

&lt;p&gt;While looking at the giant list of AWS Service Quotas, I thought it might be fun to look at the data more closely. It remains to be seen whether the unSkript team will continue to let me use PivotTables to look at data.&lt;/p&gt;

&lt;p&gt;More importantly, the list of Service quotas – with the Service Code and Quota Code are all in one table, and we have published the Feb 15, 2023 list in the &lt;a href="https://docs.unskript.com/unskript-product-documentation/lists/test" rel="noopener noreferrer"&gt;unSkript Docs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you’re interested in learning more about unSkript, join our &lt;a href="https://communityinviter.com/apps/cloud-ops-community/awesome-cloud-automation" rel="noopener noreferrer"&gt;Slack Community&lt;/a&gt;, or check out our &lt;a href="https://github.com/unskript/Awesome-CloudOps-Automation" rel="noopener noreferrer"&gt;Open Source&lt;/a&gt; repo, you can run unSkript Open Source locally with Docker!&lt;/p&gt;

</description>
      <category>aws</category>
      <category>leadership</category>
      <category>otherposts</category>
    </item>
  </channel>
</rss>
