How to Use the Kotlin DSL for Agentic Flows in Embabel-Agent

The Kotlin DSL for agentic flows provides type-safe builders like agent(), transformation(), and flow() that let you declaratively compose autonomous agents with pure Kotlin or LLM-driven logic.

The embabel/embabel-agent repository delivers a declarative, type-safe API for building agentic workflows in Kotlin. Located under com.embabel.agent.api.dsl, this domain-specific language enables developers to define agents, actions, and composable flows without sacrificing compile-time safety. Whether orchestrating simple transformations or complex parallel aggregations, the DSL abstracts runtime complexity while maintaining full access to underlying implementation details.

Core Concepts of the DSL

The Kotlin DSL for agentic flows centers on three architectural primitives that work together to create reusable, executable agents.

Agent Definition

The agent { … } block serves as the entry point for creating an Agent instance. Defined in embabel-agent-api/src/main/kotlin/com/embabel/agent/api/dsl/agent.kt, the agent() function accepts metadata (name, provider, version, description) and a configuration block that mutates an AgentBuilder【agent.kt:26‑43】. This builder collects immutable collections of actions, goals, and conditions that define the agent's behavior.

Action Building

Individual computational steps are registered through specialized builder methods on AgentBuilder:

  • transformation<I, O>() – Registers pure Kotlin functions as TransformationAction instances for type-safe data processing【AgentBuilder.kt:123‑58】.
  • promptedTransformer<I, O>() – Creates LLM-driven transformations that assemble prompts, execute model calls, and parse responses into typed outputs【AgentBuilder.kt:61‑99】.
  • flow { … } – Embeds a nested scope for complex sub-flows, returning a TypedAgentScopeBuilder that supports chaining and aggregation【AgentBuilder.kt:10‑18】.

Flow Composition

Higher-order builders in TypedAgentScopeBuilder (located in embabel-agent-api/src/main/kotlin/com/embabel/agent/api/dsl/TypedAgentScopeBuilder.kt) enable sophisticated control flow:

  • andThen() – Sequentially chains transformations, automatically wiring output types to subsequent input types.
  • aggregate() – Executes parallel transformations on identical inputs and merges results into a unified output type【TypedAgentScopeBuilder.kt:9‑31】.
  • chain() – Constructs low-level linked TransformationAction pipelines (A→B→C)【TypedAgentScopeBuilder.kt:20‑55】.
  • branch() – Creates conditional splits yielding discriminated unions for pattern matching downstream.

Architectural Deep Dive

Understanding the internal mechanics of the DSL builders ensures you can leverage advanced features like type reflection and context propagation.

The agent() Entry Point

When you invoke agent(), the DSL creates a mutable AgentBuilder scope, applies your configuration block, and invokes build() to produce an immutable Agent instance:

fun agent(
    name: String,
    provider: String = "embabel",
    version: Semver = Semver(),
    description: String,
    promptContributors: List<PromptContributor> = emptyList(),
    block: AgentBuilder.() -> Unit,
): Agent

The resulting Agent holds immutable collections of actions, goals, and conditions that the runtime executes according to the declared semantics.

AgentBuilder Mechanics

AgentBuilder maintains three critical mutable collections:

  • actions: MutableList<Action> – Populated via transformation, promptedTransformer, and flow calls.
  • goals: MutableSet<Goal> – Declared using goal(name, …) to specify completion criteria linked to output types (satisfiedBy).
  • conditions: MutableSet<Condition> – Constraints added via condition { … } or promptedCondition.

Goals declare what output type satisfies the agent's objective, enabling the platform to detect completion when that type appears on the execution blackboard.

TypedAgentScopeBuilder for Composable Flows

When flow { … } is invoked, the DSL returns a TypedAgentScopeBuilder<O> where O represents the sub-flow's output type. This scope provides type-safe chaining through reified generic parameters and Java Class reflection (e.g., A::class.java). The aggregate() method exemplifies this by accepting a list of transforms (I) -> O and a merge function (List<O>, I) -> R, enforcing type constraints at compile time while enabling parallel execution semantics【TypedAgentScopeBuilder.kt:9‑31】.

Practical Code Examples

Minimal Agent with Pure Kotlin

Create a simple echo agent using only Kotlin transformations without LLM dependencies:

import com.embabel.agent.api.dsl.agent
import com.embabel.agent.api.dsl.transformation
import com.embabel.agent.domain.io.UserInput

val HelloAgent = agent(
    name = "HelloAgent",
    description = "Echoes the user input"
) {
    transformation<UserInput, String>(name = "echo") { input ->
        "You said: ${input.text}"
    }

    goal(name = "done", description = "Finished", satisfiedBy = String::class)
}

The transformation consumes UserInput and produces String, while the goal instructs the platform to terminate execution when a String value materializes.

LLM-Driven Transformations

Use promptedTransformer to integrate large language models into your agentic flow:

import com.embabel.agent.api.dsl.agent
import com.embabel.agent.api.dsl.promptedTransformer

val Summarizer = agent(
    name = "Summarizer",
    description = "Summarizes a paragraph using the LLM"
) {
    promptedTransformer<UserInput, String>(
        name = "summarize",
        prompt = { ctx -> "Summarize this text in one sentence: ${ctx.input.text}" }
    )
    goal(name = "done", description = "Summary ready", satisfiedBy = String::class)
}

This builder constructs the prompt template, manages LLM invocation via LlmOptions, and handles response parsing back into the declared output type.

Parallel Processing with Aggregation

Execute multiple transformations concurrently and merge results using aggregate:

import com.embabel.agent.api.dsl.agent
import com.embabel.agent.api.dsl.flow
import com.embabel.agent.api.dsl.aggregate

data class MagicVictim(val name: String)
data class Frog(val name: String)
data class SnakeMeal(val frogs: List<Frog>)

val FrogAggregator = agent(
    name = "FrogAggregator",
    description = "Creates three frogs and aggregates them"
) {
    transformation<UserInput, MagicVictim>(name = "magic") { MagicVictim("Hamish") }

    flow {
        aggregate<MagicVictim, Frog, SnakeMeal>(
            transforms = listOf(
                { mv -> Frog(mv.name) },
                { _ -> Frog("2") },
                { _ -> Frog("3") }
            ),
            merge = { frogs, _ -> SnakeMeal(frogs) }
        )
    }

    goal(name = "done", description = "All frogs collected", satisfiedBy = SnakeMeal::class)
}

The aggregate builder runs three independent TransformationAction instances on the same MagicVictim input, then merges the resulting List<Frog> into a SnakeMeal instance.

Sequential Chaining

Link transformations sequentially using andThen:

val ChainedAgent = agent(
    name = "ChainedAgent",
    description = "Upper-cases then reverses a string"
) {
    transformation<String, String>(name = "upper") { it.uppercase() }
        .andThen { it.reversed() }
    goal(name = "done", description = "Processed string", satisfiedBy = String::class)
}

The andThen method appends a second step to the pipeline, automatically connecting the String output of upper to the input of the reversal transformation.

Executing Agents at Runtime

After defining an agent with the DSL, invoke it through the platform API:

import com.embabel.agent.core.*
import com.embabel.agent.domain.io.UserInput

val platform = dummyAgentPlatform() // Replace with production platform implementation
val result = platform.runAgentFrom(
    agent = HelloAgent,
    processOptions = ProcessOptions(),
    bindings = mapOf("it" to UserInput("Hello DSL!"))
)

println(result.lastResult()) // Output: "You said: Hello DSL!"

The runAgentFrom helper extracts the single action from the agent (via asAction) and executes it within the supplied TransformationActionContext, returning the typed output【TypedAgentScopeBuilder.kt:57‑68】.

Summary

  • The Kotlin DSL for agentic flows in embabel/embabel-agent provides type-safe builders under com.embabel.agent.api.dsl for constructing autonomous agents.
  • agent { … } (defined in agent.kt) creates immutable Agent instances via the mutable AgentBuilder mechanism.
  • Actions are added via transformation() for pure Kotlin logic or promptedTransformer() for LLM-augmented steps, both implemented in AgentBuilder.kt.
  • Composable flows use flow { … } returning TypedAgentScopeBuilder, enabling aggregate() for parallel processing and andThen() for sequential chaining.
  • Execution occurs through platform methods like runAgentFrom, which consume the DSL-defined agent and return typed results.

Frequently Asked Questions

What is the difference between transformation and promptedTransformer?

transformation registers pure Kotlin functions that execute deterministic logic without external service calls, while promptedTransformer constructs dynamic prompts, invokes the configured LLM through LlmOptions, and parses unstructured text responses back into typed objects. Use transformation for data validation or business logic, and promptedTransformer for natural language understanding or generation tasks.

How do I execute an agent after defining it with the Kotlin DSL?

After building an Agent instance using the DSL, pass it to the platform's execution API such as runAgentFrom(). This method requires the agent definition, ProcessOptions for runtime configuration, and an initial bindings map containing the input data. The platform extracts the agent's action graph and executes it within a TransformationActionContext, returning the final typed output.

Can I compose multiple agents together using the DSL?

Yes. The DSL supports agent composition through flow { … } blocks and specific action types like localAgentAction() or referencedAgentAction(). These methods allow you to embed existing agents as sub-components within larger workflows, treating them as atomic actions within the parent agent's execution graph while maintaining type safety across agent boundaries.

What types are supported in the DSL's type-safe builders?

The DSL uses reified generic parameters and Java Class reflection (e.g., String::class.java) to enforce type constraints at compile time. Any Kotlin type can serve as input (I) or output (O) parameters for transformations, including data classes, primitives, and generic collections. The TypedAgentScopeBuilder propagates these type parameters through chains and aggregates, ensuring that incompatible transformations fail at compile time rather than runtime.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →