TaskMonkey Handbuch

API-Features — was du deklarativ machen kannst, ohne Handler

Preprocess, mapping, collection, body_transform, _query, Placeholder — die vollständige Feature-Liste für API-Tools.

API-Features — deklarativ statt Handler

Wenn dein Tool mit einer externen HTTP-API redet: schreibe keinen Handler, wenn du es vermeiden kannst. Der ToolRunner hat einen kompletten deklarativen Layer der HTTP-Requests, Response-Umformung und Auth für dich macht.

Ein Handler ist nur dann sinnvoll wenn:

  • die Logik echte Berechnung ist (nicht nur Daten-Umformung),
  • mehrere API-Calls verkettet werden müssen,
  • oder wenn du auf $ctx-Zeug angewiesen bist das die deklarativen Features nicht abdecken.

Sonst: api + endpoint + optional mapping/preprocess/postprocess reicht.

Minimal-Beispiel — API-only, kein Handler

'getInvoice' => [
    'description' => 'Rechnung per ID abrufen.',
    'parameters' => [
        'type' => 'object',
        'properties' => ['id' => ['type' => 'string']],
        'required' => ['id'],
    ],
    'api' => 'jtl',                    // → apis.jtl aus tenantConfig
    'method' => 'GET',                 // Default: GET
    'endpoint' => '/invoices/{id}',    // {id} wird aus $args['id'] ersetzt
],

Der Runner:

  1. Löst apis.jtl.base_url + apis.jtl.headers auf (inkl. {{secret:...}}-Expansion).
  2. Ersetzt {id} im endpoint durch $args['id'].
  3. Feuert GET, gibt die JSON-Response ans LLM.

Kein Handler, kein Cake\Http\Client, kein ->getJson() — alles vom Runner erledigt.

URL-Placeholder — jeder skalare Arg ersetzt {arg_name}

Alle scalaren Argumente ersetzen automatisch {arg_name} im endpoint. Nested/Object-Args werden übersprungen.

'endpoint' => '/customers/{customer_id}/orders/{order_id}',

Wenn $args = ['customer_id' => 42, 'order_id' => 'ORD-99']: ergibt /customers/42/orders/ORD-99.

Query-String — via _query aus preprocess

Wenn du dynamische Query-Parameter brauchst (Filter, Pagination), setze sie im preprocess-Callback als _query-Array. Der Runner url-encoded das automatisch an den endpoint an.

'listOrders' => [
    'description' => 'Bestellungen auflisten (Filter: status, datum-von/bis).',
    'parameters' => [
        'type' => 'object',
        'properties' => [
            'status' => ['type' => 'string', 'enum' => ['open', 'shipped']],
            'since' => ['type' => 'string', 'description' => 'YYYY-MM-DD'],
        ],
    ],
    'api' => 'shopify',
    'method' => 'GET',
    'endpoint' => '/orders.json',
    'preprocess' => function (array $args, array $ctx): array {
        $q = [];
        if (!empty($args['status'])) $q['status'] = $args['status'];
        if (!empty($args['since']))  $q['created_at_min'] = $args['since'];
        return ['_query' => $q];
    },
],

mapping — Response-Felder umbenennen und flach ziehen

Wenn die API ein verschachteltes JSON zurückgibt und du dem LLM nur die relevanten Felder mit sauberen Namen zeigen willst:

'getInvoice' => [
    'api' => 'jtl',
    'endpoint' => '/invoices/{id}',
    'mapping' => [
        'id'       => 'invoice_id',           // → response.invoice_id
        'total'    => 'amounts.gross',        // → response.amounts.gross (dot-notation)
        'customer' => 'customer.name',
        'items'    => 'lines',
    ],
],

Response { "invoice_id": "INV-1", "amounts": { "gross": 119, "net": 100 }, "customer": { "name": "ACME" } } wird zu { "id": "INV-1", "total": 119, "customer": "ACME" }.

collection — Array-Response entnesten

Wenn die API ein Wrapping-Objekt zurückgibt (z.B. { "data": [...], "meta": {...} }) und du nur das Array willst:

'listInvoices' => [
    'api' => 'jtl',
    'endpoint' => '/invoices',
    'collection' => 'data',        // greift auf response.data zu
    'mapping' => [
        'id'    => 'invoice_id',
        'total' => 'amounts.gross',
    ],
],

collection wird BEFORE mapping angewendet — jede Zeile im Array wird dann durch das Mapping gejagt.

Dot-Notation funktioniert auch: 'collection' => 'response.items' greift auf response.items zu.

body_transform — Body vor dem POST umformen

Bei POST/PUT/PATCH werden $args per default 1:1 als Body geschickt. Wenn die API einen anderen Body-Shape erwartet:

'createTicket' => [
    'api' => 'zendesk',
    'method' => 'POST',
    'endpoint' => '/tickets.json',
    'json' => true,
    'body_transform' => function (array $args): array {
        return [
            'ticket' => [
                'subject' => $args['title'],
                'comment' => ['body' => $args['message']],
                'priority' => $args['priority'] ?? 'normal',
            ],
        ];
    },
],

$args = ['title' => 'X', 'message' => 'Y'] → Body wird zu {"ticket": {"subject":"X", "comment":{"body":"Y"}, "priority":"normal"}}.

json — JSON-Encoding aktivieren

Standardmäßig serialisiert der Runner den Body als form-encoded. Für JSON-APIs:

'createTicket' => [
    'api' => 'zendesk',
    'method' => 'POST',
    'endpoint' => '/tickets.json',
    'json' => true,          // → Content-Type: application/json, Body als JSON
],

postprocess — Response nachbearbeiten (statt Handler)

Für Berechnungen die auf dem API-Result laufen sollen — bevor der Handler-Fallback greift.

'getInvoice' => [
    'api' => 'jtl',
    'endpoint' => '/invoices/{id}',
    'postprocess' => function (array $result, array $args, array $ctx): array {
        $result['totalWithTax'] = round($result['net'] * 1.19, 2);
        return $result;
    },
],

Entscheidungs-Baum: brauche ich einen Handler?

Situation Handler nötig? Alternative
Ein HTTP-Call, Response direkt zurückgeben api + endpoint
Response-Felder umbenennen/entnesten mapping + evtl. collection
Dynamischer Query-String (Filter, Pagination) preprocess mit _query
Body für POST muss umgeformt werden body_transform + json: true
Netto → Brutto rechnen auf Response postprocess
Mehrere API-Calls verketten Handler oder Nested-Tool-Call
Datei generieren und speichern Handler mit writeFile
Reine Berechnung ohne API Handler-only
DB-Query mit ConnectionManager Handler-only

Vor jedem Handler-Prompt: frag dich ob 3-4 deklarative Zeilen dasselbe können. Wenn ja: keinen Handler.

Kompletter Feature-Katalog

Alle Keys die im Tool-Config-Array erkannt werden:

Key Typ Zweck
api string Referenz auf apis.<name> im Tenant-Config
method string GET/POST/PUT/PATCH/DELETE (Default: GET)
endpoint string Pfad, mit {arg}-Placeholder
json bool Body als JSON encoden (Content-Type: application/json)
content_type string Override für Content-Type (z.B. image/png für Media-Upload)
content_disposition_filename string Für Multipart-Uploads, {arg}-Placeholder erlaubt
preprocess callable function(array $args, array $ctx): array — merged Result in $args. Special-Key _query wird zum URL-Query-String
postprocess callable function(array $result, array $args, array $ctx): array — läuft nach API-Response, vor Mapping
body_transform callable function(array $args): array — formt POST-Body um
mapping array ['newKey' => 'response.dot.path'] — flacht/renamed Response-Felder
collection string Dot-Path zum Array in der Response (z.B. 'data' oder 'response.items')
handler callable Fallback für alles was nicht deklarativ geht. function(array $results, array $args, array $ctx): array
description string Was das Tool tut (fürs LLM)
parameters array JSON-Schema für die Args
statusMessages array Kurze Meldungen die während der Ausführung im Chat gezeigt werden
args_fixture array Default-Args für tm test-tool
internal bool Ist das Tool "intern" (nicht dem User erwähnenswert)
async bool Läuft in Background-Queue statt inline
extends string Basis-Definition aus _shared erben
Zuletzt aktualisiert: 2026-07-29