Skip to content

AI workflows, woven

AI agents and workflows for Kotlin, as small typed graphs

Write each step as a suspend function on a data class of your own, and connect the steps with arrows. Telar runs the graph on every Kotlin platform.

The Telar logo, a small loom

An agent in a few lines ​

A model that calls your functions until it can answer. toolAgent is that loop, ready-made:

kotlin
@Serializable
data class City(@Description("A city, for example \"Lisbon\"") val name: String)

// A tool: a function the model may call. Its input class becomes the schema the model reads.
val weather = Tool<City>("weather", "Returns today's weather in a city.") { city ->
    "Light rain and 17 °C in ${city.name}." // call your real service here
}

suspend fun main() {
    val model = AnthropicChatModel(HttpClient(), apiKey = System.getenv("ANTHROPIC_API_KEY"), model = "claude-opus-5-5")
    val agent = toolAgent(model, tools = listOf(weather), system = "You are a travel assistant.")

    // The agent is a graph like any other: stream it, save it, pause it before a tool runs.
    agent.stream(AgentState("Do I need an umbrella in Lisbon today?")).collect { event ->
        event.textDelta?.let { print(it) } // the answer, piece by piece
    }
}

The same agent with OpenAI, Gemini or a local Ollama model, a person who approves each tool call, and a chat that continues across turns: Agents with tools.

A workflow that waits for a person ​

When an agent alone is not enough, draw the workflow yourself. Nodes are steps, then is an arrow, and a conditional edge picks the next step by looking at the state:

kotlin
data class Refund(val orderId: String, val amount: Int, val approved: Boolean = false, val status: String = "new")

val refunds = StateGraph<Refund> {
    val check = node("check") { it.copy(status = if (it.amount > 1000) "too large" else "checked") }
    val pay = node("pay") { it.copy(status = if (it.approved) "paid" else "rejected") }
    val decline = node("decline") { it.copy(status = "declined") }

    START then check
    conditionalEdge(check, targets = setOf(pay, decline)) { if (it.status == "checked") pay else decline }
    pay then END
    decline then END
}.compile() // a node nothing leads to, or a target that is not declared, fails here

suspend fun main() {
    // Stop before money moves. The run is saved after every step.
    val config = GraphConfig(threadId = "order-1001", checkpointer = MemoryCheckpointer<Refund>(), interruptBefore = setOf("pay"))

    refunds.invoke(Refund("1001", amount = 250), config)             // runs "check", then waits before "pay"
    val result = refunds.resume(config) { it.copy(approved = true) } // a person said yes
    println(result.state.status)                                     // paid
}

With FileCheckpointer in place of MemoryCheckpointer, the run waits in a file and continues after a restart: Human-in-the-loop.

Add it to your project ​

kotlin
// build.gradle.kts
implementation("dev.deeptelar:telar-core:0.1.0-alpha09")      // graphs: all the workflow above needs
implementation("dev.deeptelar:telar-agent:0.1.0-alpha09")     // chat models, tools and the agent
implementation("dev.deeptelar:telar-anthropic:0.1.0-alpha09") // Claude. Or telar-openai: OpenAI, Gemini, Ollama, Groq, ...

Telar is in alpha: the API can still change before 1.0. Installation lists every module and its targets, and Why Telar compares it with Koog, LangGraph4j and LangChain4j.

Where to go next ​

I want toGo to
See a complete example with every line explainedQuick start
Learn graphs and agents from zero, in eight levelsTutorial
Look up one featureGuides
See graphs run before I write any codePixel Pizza, a game in the browser
Read what a function doesAPI reference

Released under the Apache License 2.0.