Getting Started with ACDL

Learn to describe LLM agent context structures in under 10 minutes

1

What is ACDL?

ACDL (Agentic Context Description Language) is a notation for describing the structure of agent contexts—the system instructions, user messages, tool calls, conversation history, and conditional logic that make up what your agent sends to the LLM.

A prompt is what gets sent to the LLM at a single point in time. A context is the pattern that describes how prompts are constructed and evolve across turns—which parts stay fixed, which parts accumulate, and how the structure changes dynamically. ACDL describes contexts, not just prompts.

Ad hoc prose or prompt excerpts are imprecise, hard to verify, and make comparing implementations difficult. ACDL provides a clean, precise notation that is easy to read and compare across different systems. As your agent grows more complex, ACDL helps you see the structure at a glance, share it with others, and reason about how context evolves over time.

2

Your First Prompt

Let's start with the simplest possible ACDL prompt. Every prompt is a series of messages, each marked with a role (System, User, Assistant, Tool) and containing different types of content. Here's what that looks like in ACDL:

HelloWorld[@T]: {
    S: INSTRUCTIONS
    U: CONTENT
}
Rendered

HelloWorld[@T]:

System
INSTRUCTIONS
User
CONTENT

This describes an llm context with two messages. The first message is a message who's role is System and whose content is the template INSTRUCTIONS. The second message is a message who's role is User and whose content is the template CONTENT.

Templates let you separate structure from content. INSTRUCTIONS might expand to a long system prompt, but in ACDL we just see the structural role it plays. This keeps specifications clean and focused on the conversation flow.
The Five Roles
ACDL has four chat role markers: S: (System), U: (User), A: (Assistant), and T: (Tool response). These map directly to the roles in chat-based LLM APIs.
3

Multi-line Messages

When a role message contains multiple elements, use braces to group them together:

CodingAssistant[@T]: {
    S: {
        SYSTEM_INSTRUCTIONS
        CODING_GUIDELINES
        AVAILABLE_TOOLS
    }
    U: env.user_input[@T]
}
Rendered

CodingAssistant[@T]:

System
SYSTEM_INSTRUCTIONS
CODING_GUIDELINES
AVAILABLE_TOOLS
User
env.user_input[@T]

The S: { ... } form lets you include multiple templates, variables, or any combination of content in a single message. Without braces, a role takes just one element.

4

Adding Variables

Real prompts usually include dynamic content. ACDL uses context variables that come from three sources:

Immutable Values
All values in ACDL are immutable. A value like sys.config.role stays the same throughout the system's lifetime. Values that change between steps are accessed with a time index: env.user_input[@T] is the user input at the current step.
with-variables.acdl
AssistantPrompt: {
    S: ROLE_DESCRIPTION(env.assistant_name)
    U: env.user_input
}
Variables like env.user_input are placeholders. When you implement your agent, you'll fill these in with actual values. ACDL doesn't care about the implementation - it just describes the structure.
5

Time Indices: Tracking Conversation Turns

Agents have conversations that evolve over multiple turns. ACDL uses time indices to describe which turn we're talking about:

ChatAgent[@T]: {
    S: ROLE  // you are a helpful assistant...
    U: env.user_input[@T]
}
Rendered

ChatAgent[@T]:

System
ROLE // you are a helpful assistant...
User
env.user_input[@T]

The [@T] after the prompt name means "this prompt is parameterized by turn T". Inside the prompt, env.user_input[@T] means "the user's input at turn T".

6

Loops: Including Conversation History

Most agents need to include previous conversation turns. Use ForEach to loop through history:

ChatWithHistory[@T]: {
    S: INSTRUCTIONS
    // Previous turns
    ForEach(@t: range(1, @T)) {
        U: env.user_input[@t]
        A: resp.reply[@t]
    }
    // Current turn
    U: env.user_input[@T]
}
Rendered

ChatWithHistory[@T]:

System
INSTRUCTIONS
// Previous turns
ForEach @t : 1 ... @T
User
env.user_input[@t]
Assistant
resp.reply[@t]
// Current turn
User
env.user_input[@T]
ForEach(@t: range(1, @T)) iterates from turn 1 up to (but not including) the current turn @T. The loop variable @t takes each value in that range.
7

Putting It Together: A ReAct Agent

Let's combine everything to describe a ReAct agent - an agent that reasons and uses tools in a loop:

ReactAgent[@T]: {
    S: {
        TASK_INSTRUCTIONS
        AVAILABLE_TOOLS
    }
    U: env.user_question
    // Action history
    ForEach(@t: range(1, @T)) {
        A: {
            resp.reasoning[@t]
            sys.tool_call[@t]
        }
        T: sys.tool_call[@t].response
    }
    S: CONTINUE_OR_ANSWER
}
Rendered

ReactAgent[@T]:

System
TASK_INSTRUCTIONS
AVAILABLE_TOOLS
User
env.user_question
// Action history
ForEach @t : 1 ... @T
Assistant
resp.reasoning[@t]
sys.tool_call[@t]
Tool
sys.tool_call[@t].response
System
CONTINUE_OR_ANSWER

This describes the classic ReAct pattern:

See this in the Live Editor
8

Conditionals: Dynamic Context

Sometimes you need different context based on conditions. Use If, ElseIf, and Else:

AdaptiveAgent[@T]: {
    S: BASE_INSTRUCTIONS

    If env.has_documents {
        S: {
            RAG_INSTRUCTIONS
            sys.retrieved_docs
        }
    }

    If env.has_tools {
        S: TOOL_INSTRUCTIONS
    } Else {
        S: NO_TOOLS_MESSAGE
    }

    U: env.user_input[@T]
}
Rendered

AdaptiveAgent[@T]:

System
BASE_INSTRUCTIONS
If env.has_documents
System
RAG_INSTRUCTIONS
sys.retrieved_docs
If env.has_tools
System
TOOL_INSTRUCTIONS
Else
System
NO_TOOLS_MESSAGE
User
env.user_input[@T]

This agent adapts its instructions based on whether documents and tools are available.

What's Next?