Treat the LLM like a typed Python function and stop fighting agent graphs
If you've been living under a rock, every framework wants you to draw an agent graph in YAML. Behold: PrefectHQ's Marvin just slaps a decorator on a function and you're done.
The Setup
Marvin's core idea: an LLM call is just a function with a signature. Pydantic in, Pydantic out, docstring is the prompt. It plays perfectly with the Cursor + Claude Code daily workflow because the types are visible everywhere.
import marvin
from pydantic import BaseModel
class Claim(BaseModel):
address: str
damage_type: str
severity: int # 1-5
@marvin.fn
def extract_claim(transcript: str) -> Claim:
"""Pull insurance claim details from a phone call transcript."""
result = extract_claim("Caller at 12 Main St, hail through the roof, bad")
print(result) # Claim(address='12 Main St', damage_type='hail', severity=4)The Money Pattern
The killer feature is composability. You stack \`@marvin.fn\` and \`@marvin.classifier\` calls like regular Python — no graph, no nodes, no event bus. The whole agent reads top-to-bottom like a script.
from typing import Literal
@marvin.classifier
def route(text: str) -> Literal["billing", "support", "sales"]:
"""Route an inbound message to the right team."""
@marvin.fn
def draft_reply(text: str, team: str) -> str:
"""Draft a short reply for the given team."""
msg = "I want a refund for last month"
team = route(msg)
reply = draft_reply(msg, team)The Catch
Marvin pulls in Prefect-flavoured dependencies and they're not small. If you're shipping a tiny serverless function, Mirascope is leaner. And the default model selection wants OpenAI — swap to Anthropic or local via \`marvin.settings\` before you ship.
The Verdict
For Pydantic-heavy codebases — which is basically every Aidxn Design backend — Marvin is the cleanest agent abstraction shipped this year. If you've been hand-rolling tool calls, give it a weekend. You won't go back to graph YAML.