,

Build a Dogwood Agent Governance Policy Step by Step

Hands typing beneath a glowing policy scroll with a clock icon and a checkmark, representing writing and validating a temporal governance policy

This is a hands-on walkthrough for Dogwood agent governance: you’ll write a temporal, Cedar-style policy that boxes in an AI agent’s action window and call rate, validate it, and wire it into a Lambda authorizer so it actually gets enforced instead of just looking correct in a text file. Dogwood was open-sourced by AWS on August 10, 2026, so treat the exact CLI flags and syntax below as illustrative and current as of this writing — check github.com/cedar-policy before you copy anything into production.

Prerequisites

  • An AWS account with permissions to create Lambda functions and, optionally, an Amazon Verified Permissions policy store
  • The Cedar CLI installed locally (cargo install cedar-policy-cli, or the prebuilt binary from github.com/cedar-policy)
  • Node.js 20+ or Python 3.12+ for the Lambda authorizer stub
  • Basic familiarity with JSON and a text editor
  • Fifteen to twenty minutes

What You’re Building With Dogwood Agent Governance

A policy that says: an agent called invoice-processor-01 may call a refund API, but only within a 30-minute session window, only up to 5 times per session, and only during business hours. Then you’ll wrap that check in a Lambda function that any agent orchestration layer — Step Functions, or a Bedrock AgentCore tool-invocation hook — can call before letting the agent act.

  1. Set up your workspace. Create a project folder and a schema file that defines your entities.

    mkdir dogwood-agent-policy && cd dogwood-agent-policy
    mkdir policies schema tests
    touch schema/agent.cedarschema.json policies/refund-agent.cedar
  2. Define the schema. Cedar (and by extension Dogwood) needs to know the shape of your entities and context before it can validate anything. Open schema/agent.cedarschema.json and add:

    {
      "PaymentsApp": {
        "entityTypes": {
          "Agent": { "shape": { "type": "Record", "attributes": {} } },
          "API": { "shape": { "type": "Record", "attributes": {} } }
        },
        "actions": {
          "InvokeAPI": {
            "appliesTo": {
              "principalTypes": ["Agent"],
              "resourceTypes": ["API"],
              "context": {
                "type": "Record",
                "attributes": {
                  "session": { "type": "Record", "attributes": {
                    "elapsedMinutes": { "type": "Long" },
                    "callCount": { "type": "Long" }
                  }},
                  "time": { "type": "Record", "attributes": {
                    "hourOfDay": { "type": "Long" }
                  }}
                }
              }
            }
          }
        }
      }
    }
  3. Write the temporal policy. Open policies/refund-agent.cedar and add the permit/forbid pair. This is the illustrative Dogwood-style syntax — exact keywords may shift as AWS finalizes the spec:

    // policies/refund-agent.cedar
    permit(
        principal == PaymentsApp::Agent::"invoice-processor-01",
        action == PaymentsApp::Action::"InvokeAPI",
        resource == PaymentsApp::API::"payments/refund"
    )
    when {
        context.session.elapsedMinutes <= 30 &&
        context.session.callCount <= 5 &&
        context.time.hourOfDay >= 9 &&
        context.time.hourOfDay < 17
    };
    
    forbid(
        principal == PaymentsApp::Agent::"invoice-processor-01",
        action == PaymentsApp::Action::"InvokeAPI",
        resource == PaymentsApp::API::"payments/refund"
    )
    when {
        context.session.callCount > 5
    };
  4. Validate the policy against the schema. Catch typos and type mismatches before they reach production.

    cedar validate \
      --schema schema/agent.cedarschema.json \
      --policies policies/refund-agent.cedar

    Expect a clean “no errors” response. If validation fails, it’ll point at the exact attribute path that doesn’t match your schema — fix that before moving on.

  5. Write test contexts for both allow and deny cases. Create tests/allow.json and tests/deny.json:

    // tests/allow.json — should permit
    {
      "principal": "PaymentsApp::Agent::\"invoice-processor-01\"",
      "action": "PaymentsApp::Action::\"InvokeAPI\"",
      "resource": "PaymentsApp::API::\"payments/refund\"",
      "context": {
        "session": { "elapsedMinutes": 12, "callCount": 3 },
        "time": { "hourOfDay": 14 }
      }
    }
    
    // tests/deny.json — should forbid (call count exceeded)
    {
      "principal": "PaymentsApp::Agent::\"invoice-processor-01\"",
      "action": "PaymentsApp::Action::\"InvokeAPI\"",
      "resource": "PaymentsApp::API::\"payments/refund\"",
      "context": {
        "session": { "elapsedMinutes": 12, "callCount": 7 },
        "time": { "hourOfDay": 14 }
      }
    }
  6. Run the authorization checks from the CLI.

    cedar authorize \
      --policies policies/refund-agent.cedar \
      --schema schema/agent.cedarschema.json \
      --request-json tests/allow.json
    # Expect: ALLOW
    
    cedar authorize \
      --policies policies/refund-agent.cedar \
      --schema schema/agent.cedarschema.json \
      --request-json tests/deny.json
    # Expect: DENY
A gate icon with a clock connected to a serverless function cube, representing a policy check wired into a Lambda authorizer
  1. Wrap the check in a Lambda authorizer. This is the piece that turns a policy file into an actual enforcement point. Any orchestration layer — a Step Functions state, or an AgentCore tool-call hook — invokes this function before letting the agent’s action through.

    // index.mjs — Lambda authorizer stub (Node.js 20)
    import { isAuthorized } from "@cedar-policy/cedar-wasm"; // illustrative import
    
    export const handler = async (event) => {
      const { agentId, action, resource, session, hourOfDay } = event;
    
      const request = {
        principal: `PaymentsApp::Agent::"${agentId}"`,
        action: `PaymentsApp::Action::"${action}"`,
        resource: `PaymentsApp::API::"${resource}"`,
        context: {
          session: { elapsedMinutes: session.elapsedMinutes, callCount: session.callCount },
          time: { hourOfDay }
        }
      };
    
      const decision = isAuthorized(request); // loads policies + schema at cold start
    
      if (decision !== "Allow") {
        return { authorized: false, reason: "Temporal or scope limit exceeded" };
      }
      return { authorized: true };
    };
  2. Deploy and smoke-test it.

    zip -r function.zip index.mjs node_modules policies schema
    
    aws lambda create-function \
      --function-name dogwood-refund-agent-gate \
      --runtime nodejs20.x \
      --handler index.handler \
      --zip-file fileb://function.zip \
      --role arn:aws:iam::<account-id>:role/dogwood-authorizer-role
    
    aws lambda invoke \
      --function-name dogwood-refund-agent-gate \
      --payload '{"agentId":"invoice-processor-01","action":"InvokeAPI","resource":"payments/refund","session":{"elapsedMinutes":12,"callCount":3},"hourOfDay":14}' \
      response.json && cat response.json

    You should see {"authorized":true}. Bump callCount past 5 in the payload and re-run — you should get {"authorized":false,...} back. That’s your temporal policy, live, gating a real function call.

  3. Point your agent orchestration at the gate. If you’re running on Bedrock AgentCore, register this Lambda as a pre-tool-invocation check. If you’re using Step Functions, drop it in as a Task state ahead of any state that calls the refund API, and branch on the authorized field. For centralized management across many agents, consider fronting this with Amazon Verified Permissions so you’re not hand-rolling policy storage yourself.

Troubleshooting

  • Validation fails with a type mismatch: double-check that every attribute referenced in when {} exists in the schema with the exact type you’re comparing against — Cedar’s validator is strict on purpose, and it’s cheaper to fix a schema typo now than to debug a silent deny in production later.
  • The Lambda returns authorized: false when you expected true: print the exact context object you’re passing before it hits isAuthorized. The most common cause is a session counter that never got reset between test runs, which will make every subsequent request look like it’s already over budget.
  • Cold-start latency on the authorizer: loading policies and schema at cold start (as the stub above does) keeps invocations fast after the first one, but if you’re calling this Lambda synchronously on every single agent action, consider provisioned concurrency so the very first refund request of the day isn’t the one that eats the cold-start penalty.

What You Built

You now have a working, tested temporal policy — not just a document describing intent, but a Cedar/Dogwood-style rule that a Lambda function actively enforces before an agent’s refund call goes through. It caps the agent’s session to 30 minutes, its call volume to 5 requests, and its operating hours to 9-to-5. Swap the schema, principal, and thresholds for whatever action your own agents perform, and you’ve got a repeatable pattern: write the policy, validate it against a schema, test both the allow and deny paths, then wire it into whatever gate sits between your agent and the real world.

At Enkompass (enkompass.net), this is our home turf. We pressure-test new AWS services in real client environments the week they drop, so you get the upside without becoming the cautionary tale. We can have this exact pattern running against your agent fleet before your next sprint planning meeting. If you’d rather have seasoned cloud people handle it instead of learning the hard way at 2 AM, let’s talk. Reach out through our contact page or call us directly at +1-828-436-5667 — we’ll help you turn this from a someday-project into a done-deal.

Ready when you are

Let’s map your route to AWS

A free 60-minute architecture review with a senior engineer. No slides, no SDR, just a plan.