DEV Community

Aaron VanSledright
Aaron VanSledright

Posted on

I Replaced My Agent Framework With Markdown Files and 140 Lines of Python

Every AI agent framework I tried added complexity I didn't need. LangChain, CrewAI, AutoGen — they're powerful, but for deploying a Slack bot that answers questions using a few tools, I was pulling in hundreds of dependencies to do something boto3 already handles natively.

So I built something different: a Terraform module where agent behavior lives in markdown files, tools are plain Python functions, and the entire runtime engine is ~140 lines of code with zero external dependencies.

I open-sourced it: terraform-module-markdown-agent

The Problem With Agent Frameworks

Most agent frameworks want to own your entire stack. You get:

  • Heavyweight dependencies — hundreds of packages for what amounts to a loop calling an LLM
  • Framework lock-in — custom decorators, base classes, and abstractions that couple your business logic to the framework
  • Deployment friction — designed for containers or servers, not serverless
  • Opaque behavior — hard to debug when the agent does something unexpected because the prompt is buried in framework internals

If you're running agents on AWS Lambda with Bedrock, you already have boto3. The Bedrock Converse API handles tool use natively. The framework is mostly just getting in the way.

The Core Idea: Markdown as Configuration

What if agent behavior was just a markdown file?

---
name: support-agent
version: 1.0.0
description: "Handles customer support queries"
tags: [support, customer]
---

# Support Agent

## When to Use
Activated for all customer-facing support requests.

## Process
1. Greet the customer
2. Use `search_docs` to find relevant documentation
3. If the issue requires escalation, use `create_ticket`
4. Summarize the resolution

## Guardrails
- Never share internal pricing or roadmap details
- Always confirm before creating tickets
- Keep responses under 3 paragraphs
Enter fullscreen mode Exit fullscreen mode

This markdown file is the system prompt. The frontmatter provides metadata for routing. The sections give the LLM structured instructions. You can read it, diff it, review it in a PR — no code changes needed to adjust agent behavior.

How the Runtime Works

The engine is a simple loop:

  1. Load the skill markdown file as the system prompt
  2. Append any shared rules (company context, formatting guidelines)
  3. Call bedrock-runtime.converse() with the user message and tool specs
  4. If the model wants to use a tool, route it to the handler function
  5. Feed the tool result back and loop
  6. Return the final text response

Here's the actual function signature:

from runtime.engine import run_agent

result = run_agent(
    skill_name="support-agent",
    user_input="I can't log in to my account",
    tool_specs=my_tool_specs,
    tool_handler=my_handler,
    history=conversation_history,
)
Enter fullscreen mode Exit fullscreen mode

The full engine handles Bedrock throttling with exponential backoff, safe error messages (no internal details leaked to users), a max-turns safety limit, and S3 or local filesystem skill loading. And it does all of this in ~140 lines using only boto3.

Tools Are Just Functions

No decorators. No base classes. Define a JSON schema for Bedrock's tool spec, write a Python function, register it:

# tools/specs/support.py
SUPPORT_TOOL_SPECS = [
    {
        "toolSpec": {
            "name": "search_docs",
            "description": "Search the knowledge base",
            "inputSchema": {
                "json": {
                    "type": "object",
                    "properties": {
                        "query": {"type": "string", "description": "Search query"}
                    },
                    "required": ["query"]
                }
            }
        }
    }
]
Enter fullscreen mode Exit fullscreen mode
# tools/support.py
def search_docs(query: str) -> str:
    # Your actual search logic here
    results = my_search_index.query(query, limit=5)
    return json.dumps(results)
Enter fullscreen mode Exit fullscreen mode
# tools/registry.py
TOOL_HANDLERS = {
    "search_docs": lambda name, inp: search_docs(**inp),
}
Enter fullscreen mode Exit fullscreen mode

That's it. The registry is a dictionary. The spec is JSON. The handler is a function. You can test each piece independently.

Multi-Agent Delegation

A coordinator skill can delegate to specialized sub-skills:

---
name: coordinator
version: 1.0.0
description: Routes requests to specialized agents
---

# Coordinator

## Process
1. Analyze the user's request
2. Delegate to `support-agent` for customer issues
3. Delegate to `ops-agent` for infrastructure questions
4. Handle general conversation directly
Enter fullscreen mode Exit fullscreen mode

The delegate_to_skill tool handles the routing. Recursion depth is limited (default: 3 levels) to prevent infinite loops between skills.

What Terraform Deploys

The module provisions everything you need:

Resource Purpose
Lambda Function + Layer Agent runtime
IAM Role Least-privilege Bedrock + DynamoDB access
API Gateway (optional) HTTP endpoint for Slack webhooks
DynamoDB Table (optional) Thread-based conversation memory
EventBridge Rules (optional) Scheduled agent tasks (cron)
module "agent" {
  source = "github.com/45squaredLLC/terraform-module-markdown-agent"

  name        = "support-agent"
  environment = "prod"

  source_dir       = "${path.module}/src"
  layer_path       = "${path.module}/dist/layer.zip"
  bedrock_model_id = "us.anthropic.claude-sonnet-4-5-20250929-v1:0"

  ssm_parameter_prefixes = ["/support-agent/slack/*"]

  enable_api_gateway  = true
  enable_memory_table = true
}
Enter fullscreen mode Exit fullscreen mode

terraform apply and you have a working agent with an HTTPS endpoint, conversation memory, and IAM policies scoped to exactly what it needs.

Conversation Memory

DynamoDB stores conversation history per Slack thread:

  • Partition key: THREAD#{thread_id}
  • Sort key: MSG#{timestamp}#{uuid} (collision-safe)
  • TTL: Auto-expires after 30 days (configurable)
  • Cap: 100 messages per thread to stay within context windows

The runtime loads history automatically when processing a message in an existing thread. No session management code needed.

Scheduled Agents

Need an agent that runs on a cron schedule? EventBridge handles it:

scheduled_tasks = [
  {
    name                = "daily-report"
    schedule_expression = "cron(0 13 * * ? *)"
    input = {
      source        = "scheduled"
      task          = "daily-report"
      slack_channel = "C123ABC"
      prompt        = "Generate the daily operations summary"
    }
  }
]
Enter fullscreen mode Exit fullscreen mode

Same agent, same skills, same tools — just triggered by a schedule instead of a Slack message.

Project Structure

src/
├── orchestrator/
│   ├── handler.py        # Lambda entry point
│   └── agent.py          # Wires skills + tools
├── runtime/              # Provided by the module
│   ├── engine.py         # ~140-line Bedrock Converse loop
│   ├── handler.py        # Slack event handling
│   ├── memory.py         # DynamoDB conversation store
│   └── delegation.py     # Skill-to-skill routing
├── skills/
│   ├── coordinator.md    # Entry point skill
│   └── support-agent.md  # Domain skill
├── rules/
│   └── formatting.md     # Shared context
└── tools/
    ├── registry.py       # Tool routing
    ├── specs/
    │   └── support.py    # Tool JSON schemas
    └── support.py        # Tool implementations
Enter fullscreen mode Exit fullscreen mode

Changing agent behavior = editing a markdown file. Adding a tool = writing a function + JSON schema. No framework upgrades, no breaking API changes.

Security

A few things I cared about getting right:

  • IAM scoping: Policies are locked to the deployment region and specific resource ARNs. Bedrock access is limited to Anthropic models only.
  • Skill validation: Skill names are regex-validated to prevent path traversal. S3-loaded skills are size-limited to 1MB.
  • Tool error isolation: Internal errors return only the exception type to the model — no stack traces or secrets leak into responses.
  • Slack verification: HMAC-SHA256 signature verification runs before any event processing.
  • SSM least-privilege: Lambda can only read the specific SSM parameter prefixes you declare.

When To Use This (and When Not To)

Good fit:

  • Slack bots and chat agents on AWS
  • Agents with a handful of well-defined tools
  • Teams that want agent behavior in version-controlled markdown
  • Serverless-first deployments

Look elsewhere if:

  • You need multi-model orchestration (different LLMs per step)
  • Your agent requires complex stateful workflows with branching
  • You're not on AWS or don't want Bedrock

Getting Started

# Clone the example
git clone https://github.com/AIOpsCrew/terraform-module-markdown-agent
cd terraform-module-markdown-agent/examples/slack-bot

# Build the Lambda layer
bash ../../scripts/build_layer.sh .

# Deploy
terraform init
terraform apply
Enter fullscreen mode Exit fullscreen mode

The example includes a working Slack bot with get_time and get_weather tools. Swap the skills and tools for your use case.


The repo is Apache 2.0 licensed. If you're building agents on AWS and tired of fighting frameworks, give it a look: [github.com/AIOpsCrew/terraform-module-markdown-agent(https://github.com/AIOpsCrew/terraform-module-markdown-agent)

Questions or feedback? Drop a comment or open an issue.

Top comments (0)