I am still watching the same pattern play out every time a team tries to get an agent to answer a real business question. The agent calls one tool, then another, then a third, then tries to stitch the three responses together in a prompt. Sometimes it works. Often it does not. Always it is expensive.
This is where the fifth Naftiko Framework use case earns its keep. Compose AI context. Combine data from multiple APIs into one capability to deliver richer, task-ready context to AI clients.
Fifth post of nine. Same three-dimensional lens I have been using — technology, business, politics.
The problem is not the APIs. It is the orchestration surface.
A classic example. A support agent wants to answer “how healthy is this customer?” That question lives across at least three systems — billing (are they paid up?), CRM (who are they, what tier?), and support (how many open tickets, what severity?).
In a glue-code world you give the agent three tools. get-billing-status. get-crm-profile. list-open-tickets. The agent has to know to call all three. It has to pass the customer ID through each one. It has to reconcile the results in its own head and then try to produce a coherent answer.
Every one of those steps is a place for the model to get it wrong. Wrong ID propagated. Missing call. Confabulated join. And every one of those steps is a round trip the user is paying for — in latency and in tokens.
The question is not “how do we give the agent better tools?” The question is “why is the agent doing orchestration at all?”
Steps belong in the capability, not in the prompt
The Naftiko capability spec has a steps construct for exactly this. Inside an exposed tool you declare an ordered list of steps — call a consumed operation, lookup across previously fetched data — and wire their outputs together with with (input injection) and mappings (JSONPath bridging from prior step outputs).
The agent sees one tool. One input. One composed output. The orchestration is in Git, reviewed, linted, and owned by the platform team.
Here is the shape of a customer-health capability that pulls from three sources and returns one composed output model.
naftiko: "1.0.0-alpha1"
info:
title: Customer Health
description: "Composed customer health view across billing, CRM, and support"
capability:
consumes:
- namespace: billing
type: http
baseUri: "https://api.billing.internal"
authentication:
type: bearer
token: ""
resources:
- name: accounts
path: "/v1/accounts/{accountId}"
operations:
- name: get-billing-status
method: GET
inputParameters:
- name: accountId
in: path
type: string
required: true
- namespace: crm
type: http
baseUri: "https://api.crm.internal"
authentication:
type: bearer
token: ""
resources:
- name: customers
path: "/v2/customers/{customerId}"
operations:
- name: get-crm-profile
method: GET
inputParameters:
- name: customerId
in: path
type: string
required: true
- namespace: support
type: http
baseUri: "https://api.support.internal"
authentication:
type: bearer
token: ""
resources:
- name: tickets
path: "/v1/tickets"
operations:
- name: list-open-tickets
method: GET
inputParameters:
- name: accountId
in: query
type: string
required: true
exposes:
- type: mcp
namespace: customer-health
tools:
- name: get-customer-health
description: "Composed customer health across billing, CRM, and open support tickets"
hints:
readOnly: true
idempotent: true
inputParameters:
- name: customerId
type: string
required: true
steps:
- name: profile
call: crm.get-crm-profile
with:
customerId: ""
- name: billing
call: billing.get-billing-status
with:
accountId:
mapping: "$.steps.profile.accountId"
- name: tickets
call: support.list-open-tickets
with:
accountId:
mapping: "$.steps.profile.accountId"
outputParameters:
- name: customerId
type: string
mapping: "$.inputParameters.customerId"
- name: name
type: string
mapping: "$.steps.profile.name"
- name: tier
type: string
mapping: "$.steps.profile.tier"
- name: billing
type: object
properties:
- name: status
type: string
mapping: "$.steps.billing.status"
- name: balance
type: number
mapping: "$.steps.billing.balance"
- name: daysPastDue
type: integer
mapping: "$.steps.billing.daysPastDue"
- name: openTickets
type: array
mapping: "$.steps.tickets.tickets"
items:
type: object
properties:
- name: ticketId
type: string
mapping: "$.id"
- name: severity
type: string
mapping: "$.severity"
- name: openedAt
type: string
mapping: "$.openedAt"
- name: composedAt
type: string
const: "runtime"
One tool. Three consumes. Three ordered steps. One composed output. The agent asks once, the capability orchestrates, the model gets a clean object.
Three dimensions, one composed view
Technology. This is where orchestration stops being a prompt-engineering problem and starts being a declared contract. The with block feeds step inputs — either literal values or JSONPath expressions that point at previous step outputs. The mappings under each output parameter pull fields from any prior step by name. The engine manages the call graph. The agent does not know, and should not need to know, that three systems are involved.
Business. The conversation with leadership changes when the composed view is a file. Someone can read get-customer-health.yaml and see every upstream system that contributes to a “customer health” answer. Compliance knows which systems are touched. Data teams know which fields are exposed. Nothing is hidden inside a prompt. Nothing is hidden inside bespoke adapter code.
Politics. This is the part most teams are not ready for. When orchestration lives in capabilities, the platform team owns the question “what does customer health mean?” Not the agent team. Not whichever product team shipped their own version last quarter. The composed output becomes a shared definition, version-controlled and reviewable. The political win is not technical — it is that a cross-system concept finally has a home.
Features that matter for this one
From the wiki for use case 5:
- Multiple consumed adapters with unique namespaces, each with its own auth, baseUri, and operations
- Ordered steps with
call(invoke a consumed operation) andlookup(in-memory join) kinds - Step output bridging via
withfor input injection and JSONPathmappingsfor cross-step data flow - Composed output model with named parameters drawing from any step
- Nested objects and typed arrays in the composed output
- Const values for computed or runtime-injected fields
These are the building blocks for a capability that replaces a chain of prompt-orchestrated tool calls with one reviewed, lintable file.
Where I am using this
Every partner capability I have been writing lately runs into this. An integration against a single system is easy. The interesting capabilities — the ones that actually answer a business question — almost always cross two or three systems. Customer health. Order status that has to hit the warehouse. Compliance checks that touch identity and payment at once.
I used to ship those as multiple tools and hope the agent stitched them together. I do not do that anymore. If a single business question spans multiple systems, the spec composes across them.
Next
Next post in the series is Rightsize a set of microservices. Same lens. What it looks like technically. What it changes for the platform team. And what it quietly shifts politically inside the organization.
- Wiki: Guide — Use Cases
- GitHub: github.com/naftiko/framework
- Fleet Community Edition: github.com/naftiko/fleet
Still walking the list.