Alle Insights
    Engineering28. Juli 2026· 10 Min. Lesezeit· von Christoph-Thomas Abs

    KI-Agenten mit TypeScript bauen — und jeden Run nachvollziehen können

    KI-Agenten mit TypeScript zu bauen ist heute nicht langsamer als ein No-Code-Workflow. Der Unterschied liegt in der Nachvollziehbarkeit: Wie sieht man in vier Wochen, warum ein Run schiefging? Wie wir das bei apis.immo mit Mastra gelöst haben — typisiert, traceable, produktionsreif.

    ENGINEERING
    KI-Agenten mit TypeScript bauen — und jeden Run nachvollziehen können
    ReachOutSoftwareInsights · reachout.software
    Kurz gesagt

    KI-Agenten mit TypeScript zu bauen heißt: Agent, Werkzeuge und Workflows werden im Code deklariert statt in einer Oberfläche verdrahtet. Ein Agent ist bei Mastra eine Deklaration aus Instructions, Modell, Tools und Memory; jeder Lauf erzeugt einen durchsuchbaren Trace aus Modell-Aufrufen, Tool-Calls, Latenz, Tokens und Kosten. Der Unterschied zu No-Code liegt nicht in der Baugeschwindigkeit, sondern darin, dass vier Wochen später noch beantwortbar ist, warum ein bestimmter Lauf schiefging. Faustregel: Läuft die Automatisierung neben dem Produkt, reicht n8n — ist die KI Teil des Produkts, gewinnt Code.

    Die ehrlichste Frage, die wir zu KI-Features bekommen, kommt meistens von jemandem, der schon automatisiert hat: „Warum baut ihr das nicht einfach in n8n?“

    Berechtigte Frage — und die Antwort ist nicht „weil Code besser ist”. Sie hängt davon ab, ob die KI neben dem Produkt läuft oder darin. Bei apis.immo, unserer Plattform für Immobilieneigentümer, ist sie Produktbestandteil: ein Chat-Assistent für Eigentümer und eine Extraktions-Pipeline, die aus hochgeladenen WEG-Dokumenten strukturierte Daten macht. Dieser Artikel zeigt, wie das technisch aussieht.

    Warum nicht einfach n8n?

    Kurz vorweg, weil die Frage berechtigt ist: n8n bringt Logging, Integrationen und Tracing ab Werk mit. Jeder Node loggt, jeder Fehllauf lässt sich öffnen, und man sieht sofort, an welchem Schritt es hing. Für einen stabilen Prozess neben dem Produkt — Formular rein, Daten anreichern, CRM-Eintrag, Slack-Nachricht — ist das schwer zu schlagen. Wir raten selbst dazu, wenn kein Entwickler das Ding später pflegen soll.

    Die Grenze verläuft nicht bei „zu komplex”. Sie verläuft dort, wo eigene Datenmodelle, Domänenlogik und dieselbe Deploy-Pipeline wie der Rest des Produkts ins Spiel kommen. Ab da arbeitet man gegen das Werkzeug statt mit ihm — und genau da fängt dieser Artikel an.

    n8n oder eigener Code — woran die Entscheidung wirklich hängt
    Kriteriumn8nTypeScript-Code (Mastra)
    Bezug zum Produktläuft neben dem Produktläuft im Produkt
    Logging und Tracingab Werk, pro Nodeab Werk über die Observability des Frameworks
    Datenmodelln8n-eigene Struktureneigene Domänen-Typen, Zod-validiert
    Deployeigene n8n-Instanzdieselbe Pipeline wie der Rest des Produkts
    Wartungauch ohne Entwickler möglichsetzt ein Entwicklerteam voraus
    Auswertung der Läufemanuell im AusführungsverlaufScorer über gespeicherte Traces

    Wie sieht ein KI-Agent mit TypeScript konkret aus?

    Ein Mastra-Agent ist eine Deklaration: Anweisungen, Modell, Werkzeuge, Memory. Kein Drumherum.

    import { Agent } from "@mastra/core/agent";
    import { Memory } from "@mastra/memory";
    
    export const propertyOwnerAgent = new Agent({
      id: "property-owner-agent",
      name: "Property Owner Assistant",
      instructions: `Du beantwortest Fragen von Wohnungseigentümern
        zu Finanzen, Vorgängen und Dokumenten ihrer Einheit.`,
      model: "openai/gpt-4o-mini",
      memory: new Memory({ options: { lastMessages: 50 } }),
      tools: { financeTool, activitiesTool, documentsTool },
    });

    Die Werkzeuge sind der interessante Teil. Ein Tool ist eine typisierte Funktion mit Zod-Schema — das Modell bekommt nie freien Zugriff, sondern nur eine Liste dessen, was es aufrufen darf:

    import { createTool } from "@mastra/core/tools";
    import { z } from "zod";
    
    export const financeTool = createTool({
      id: "get-unit-finances",
      description: "Liefert Finanzdaten zur Einheit des Nutzers",
      inputSchema: z.object({ period: z.enum(["current", "last-year"]) }),
      execute: async ({ context, runtimeContext }) => {
        const unitId = runtimeContext.get(UNIT_SCOPE_KEY);
        return findFinancesForUnit(unitId, context.period);
      },
    });

    Der Unit-Scope kommt aus dem Request-Kontext, nicht aus dem Prompt. Das Modell kann also gar nicht auf fremde Einheiten zugreifen, egal wie kreativ jemand fragt. Autorisierung gehört in den Code, nicht in die Systemanweisung.

    Zusammengefasst beschreiben vier Felder den Agenten vollständig: Instructions, Modell, Memory, Tools. Alles Weitere, was sein Verhalten bestimmt, steckt in den Tools — und die sind gewöhnliche TypeScript-Funktionen mit Zod-Schema. Genau deshalb lässt sich ein Agent wie normaler Anwendungscode reviewen, testen und versionieren, statt als Konfiguration in einer fremden Oberfläche zu leben.

    Wie hält man mehrere Agenten in einem Monorepo zusammen?

    Bei uns liegt alles in einer Nx-Library, `@apis/agents`. Eine Composition Root registriert Agents, Workflows, Tools, Scorer und Storage an genau einer Stelle:

    import { Mastra } from "@mastra/core";
    import { PostgresStore } from "@mastra/pg";
    
    export const mastra = new Mastra({
      agents: { propertyOwnerAgent, fileGroupingAgent,
                propertyDataAgent, accountingAgent },
      workflows: { propertyOnboardingExtraction },
      scorers: { germanScorer, concisenessScorer, relevancyScorer },
      storage: new PostgresStore({ schemaName: "mastra" }),
      observability: { default: { enabled: true } },
      server: { port: 4111 },
    });

    Diese eine Instanz importieren zwei Anwendungen: die Eigentümer-App für Chat und Threads, der Onboarding-Service für den Extraktions-Workflow. Ein Agent, eine Definition, überall dieselben Traces. Zusätzlich läuft dieselbe Library als eigenes Container-Image, damit das Mastra-Studio gegen die echte Registry arbeitet — Library und Deployable in einem Paket, was ungewöhnlich aussieht, sich hier aber auszahlt.

    Der Nutzen zeigt sich beim Ändern: Ein neues Tool wird einmal registriert und steht sofort jedem Agenten und jedem Workflow zur Verfügung, ohne dass eine zweite Anwendung nachgezogen werden muss. Und weil die Registry typisiert ist, bricht ein umbenanntes Tool im Build — nicht erst zur Laufzeit beim Kunden.

    TYPISIERTER SCHREIBPFADAgentTypisierter Tool-Callvalidiert & sicherDB✓ ERLAUBTAgentFreitext-JSONunvalidiertDB✕ BLOCKIERTReachOutSoftware

    Wie verhindert man, dass ein Agent frei in die Datenbank schreibt?

    Das ist die Stelle, an der die meisten Agenten-Projekte kippen — und der Punkt, an dem sich Custom Code am deutlichsten auszahlt.

    Naheliegend wäre: Agent extrahiert Daten, antwortet mit JSON, Anwendung schreibt das JSON in die Datenbank. Funktioniert im Happy Path und bricht bei der ersten halluzinierten Struktur.

    Wir trennen deshalb hart. Der Agent liefert seine Extraktion über einen Tool-Call, nicht als Freitext. Der Tool-Call wird abgefangen und typisiert eingesammelt:

    const run = await agent.generate(prompt, {
      toolChoice: "required",
      runtimeContext,
    });
    
    const extracted = captureToolResult(run, extractPropertyDataTool);
    if (!extracted.ok) return markStepFailed(extracted.issues);
    
    await importPropertyData(extracted.value);  // deterministisch, keine LLM-Beteiligung

    Der Schreibvorgang selbst ist gewöhnlicher TypeScript-Code: validiert, idempotent, testbar. Das Modell entscheidet, was extrahiert wird — nie, wie es persistiert wird. Nebeneffekt: Der Capture-Mechanismus ist race-safe, sodass sich mehrere parallele Läufe eine Agent-Instanz teilen können, ohne sich gegenseitig die Ergebnisse zu überschreiben.

    Wie sieht ein Workflow aus, der Fehler übersteht?

    Unsere Onboarding-Pipeline verarbeitet Dokumente in Etappen: OCR und Klassifikation je Datei, dann Seiten zu Themengruppen bündeln, dann drei Extraktionen parallel, dann Abgleich.

    export const propertyOnboardingExtraction = createWorkflow({
      id: "property-onboarding-extraction",
      inputSchema: onboardingInputSchema,
    })
      .then(resolveDocuments)
      .foreach(processDocument, { concurrency: 3 })
      .then(groupPages)
      .parallel([extractMasterData, extractActivities, statementProcessing])
      .then(reconcileImports)
      .then(markComplete)
      .commit();

    Der spannendste Schritt ist `statementProcessing`. WEG-Abrechnungen sind uneinheitlich, ein Durchlauf reicht selten. Also läuft dort eine Schleife: Agent strukturiert, Validierung prüft, bei Fehlern geht es mit konkreten Hinweisen zurück in den Agenten — bis die Prüfung besteht oder das Pass-Limit greift.

    .dountil(
      extractAndValidateStatement,
      async ({ inputData }) =>
        inputData.isValid || inputData.passes >= MAX_PASSES,
    )

    Der Zustand liegt dabei im Storage, nicht im Prozessspeicher. Ein abgestürzter Container verliert keinen halben Import — der Lauf wird an derselben Stelle fortgesetzt.

    Für den Betrieb heißt das zweierlei: Ein Deploy mitten in der Verarbeitung kostet keinen kompletten Neu-Import, und ein einzelnes unlesbares Dokument reißt nicht den ganzen Lauf mit — der betroffene Schritt wird als fehlgeschlagen markiert, der Rest läuft weiter. Fehlschläge werden damit zu Daten, die man auswerten kann, statt zu Support-Tickets.

    Wie werden aus Traces Evaluationen?

    Jeder dieser Schritte erzeugt einen Trace: Modell-Aufruf, Tool-Call, Übergabe, Latenz, Token, Kosten — in einer durchsuchbaren Timeline. Damit ist beantwortbar, was das Modell getan hat, auf welches Werkzeug es gegriffen hat und warum es dieses gegriffen hat.

    Interessant wird das erst mit Bewertung. Scorer sind automatisierte Prüfungen über Runs — modellgestützt, regelbasiert oder statistisch, asynchron und mit einstellbarer Stichprobe. Wir haben drei eigene: einen für die Sprache der Antwort (tatsächlich Deutsch statt Denglisch oder Englisch), einen für Knappheit, einen für Relevanz.

    export const germanScorer = createScorer({
      id: "german-language",
      description: "Prüft, ob die Antwort auf Deutsch verfasst ist",
    })
      .generateScore(({ run }) => detectLanguage(run.output) === "de" ? 1 : 0);

    Aus jedem Ergebnis lässt sich ein Datensatz-Eintrag speichern. Über Wochen entsteht eine Testsammlung aus echten Läufen statt aus ausgedachten Beispielen — und seit Kurzem lassen sich gespeicherte Traces auch nachträglich bewerten, ohne den Agenten erneut laufen zu lassen.

    Der praktische Effekt: Wenn wir ein Modell tauschen, ist „ist es besser geworden?” beantwortbar. Vorher war das eine Meinung. Damit lässt sich die Performance verschiedener Modelle auf unseren konkreten Use Case messen und vergleichen — statt sie nach Benchmarks oder Bauchgefühl zu bewerten.

    Warum läuft unser R&D über die Claude-Subscription?

    Weil Kostenangst der schnellste Weg ist, Forschung auszubremsen. Wenn bei jedem Experiment eine vierstellige Rechnung mitläuft, testet man vorsichtiger und findet weniger.

    Mastra kann die Agent-SDKs von Claude, Cursor und Codex als Subagenten führen. Sie erben die normale Agenten-Oberfläche, lassen sich an Workflows übergeben, im Studio anchatten und mit Scorern bewerten — inklusive Logs, Traces und Kosten. Bei uns hängt das an einer Umgebungsvariable, sodass derselbe Workflow-Schritt beide Wege bedienen kann:

    export const accountingAgent =
      runtime === "claude-sdk"
        ? new ClaudeSDKAgent({ id: "accounting-agent", sdkOptions: { model } })
        : new Agent({ id: "accounting-agent", model, tools, instructions });

    Wir nutzen das nicht als Coding-Assistenz, sondern als Modell-Backend für unsere eigenen Agenten, solange wir in der Frage „trägt der Ansatz überhaupt?” stecken. Man bekommt nicht jede Funktion einer direkten Provider-API — für ein belastbares Bild reicht es. Kostenoptimierung gehört ans Ende eines Prozesses, nicht an den Anfang.

    Für produktive Kundensysteme läuft das anders: saubere API-Keys, saubere Abrechnung. Die Subscription ist unsere Forschungsumgebung, kein Sparmodell für Produktion.

    Wollt ihr ein KI-Feature, das ihr auch verstehen könnt?

    Wir bauen KI-Funktionen als Software statt als Klickstrecke: typisiert, nachvollziehbar, in eurer Infrastruktur. Wie das bei uns aussieht, steht auf Claude-first KI — warum ein Prototyp dafür nicht reicht, hier. Oder ihr schreibt uns direkt, was ihr vorhabt: Wir sagen ehrlich, ob es dafür Code braucht oder ob n8n reicht.

    Häufige Fragen

    Was ist Mastra?

    Ein quelloffenes TypeScript-Framework für KI-Anwendungen und Agenten. Es bündelt Agents, Workflows, Memory, Tools und Observability und läuft eingebettet in React-, Next.js- oder Node-Projekten oder als eigenständiger Server.

    Braucht man für KI-Agenten mit TypeScript zwingend ein Framework?

    Nein. Der Aufwand entsteht nicht beim ersten Agenten, sondern beim fünften — spätestens dann baut man Tracing, Evaluation und Werkzeug-Verwaltung selbst nach.

    Wie verhindert man halluzinierte Datenbank-Einträge?

    Indem der Agent Ergebnisse über typisierte Tool-Calls liefert und der Schreibvorgang deterministischer Code bleibt. Das Modell entscheidet über den Inhalt, nie über die Persistenz.

    Ist das eine Alternative zu n8n?

    Nur teilweise. n8n ist ein visuelles Automatisierungswerkzeug, Mastra eine Code-Bibliothek. Läuft die Automatisierung neben dem Produkt, ist n8n oft die bessere Wahl. Ist die KI Teil des Produkts, gewinnt Code.

    Was kostet der Einstieg?

    Der Framework-Kern ist Open Source unter Apache 2.0. Kosten entstehen durch Modell-Nutzung und optional durch die gehostete Plattform.

    Christoph-Thomas Abs
    Christoph-Thomas Abs
    Technische Leitung · ReachOut Software
    Connect on LinkedIn
    Wie viele Sprints braucht das?

    Vom Thema zum Scope — transparent im Sprint.

    Ein Thema wie dieses übersetzen wir in konkrete Tickets mit geschätzten Stunden. Meist ein bis zwei 14-Tage-Sprints — planbar, jederzeit kündbar, jede Stunde im Kanban sichtbar.

    Setup & ArchitekturKernfunktion bauenSicherheit & ReviewGo-Live
    Go-to-Market

    Software ohne Markteintritt bleibt Code.

    Für das Go-to-Market empfehlen wir unsere Schwester Prometheus Marketing – am selben Kudamm, im selben Sprint-Takt. ABM-Ziellisten, LinkedIn- & Microsoft-Ads, Outbound: Ihre ersten Kunden und Partner.

    Prometheus Marketing