HttpCalls enable EDDI agents to integrate with external REST APIs, making EDDI a powerful orchestration layer that can combine conversational AI with traditional backend services. This is how agents can fetch real-time data, authenticate users, store information in external systems, or trigger business workflows.
HttpCalls are lifecycle tasks that execute during the agent's processing pipeline:
User Input → Parser → Behavior Rules → HttpCalls → Output Generation
Typically, Behavior Rules decide when to make an API call by triggering an action like httpcall(weather-api), and the HttpCalls extension defines how to make that call.
- Fetching external data: Weather, stock prices, product information, etc.
- Authentication: OAuth flows, token validation, user verification
- CRM Integration: Creating tickets, updating customer records, searching databases
- Business workflows: Processing payments, sending notifications, triggering events
- Multi-step APIs: First call gets auth token, second call uses it to access protected resources
- Analytics: Sending conversation data to external analytics platforms
- Self-modification: The Platform Operator calls EDDI's own API to create other agents, through configs generated from EDDI's OpenAPI spec by
McpApiToolBuilder. Those are the same documents this page describes: the store and URI call themapicalls, the workflow step and file extension sayhttpcalls(see Agent Config Authoring)
- Template-based: Use conversation memory in URLs, headers, and body (e.g.,
{context.userName}) - Response handling: Save JSON responses to memory for use in outputs or subsequent calls
- Chaining: One HttpCall's response can be used in another HttpCall
- Quick reply generation: Automatically create quick reply buttons from API response arrays
- Property extraction: Extract specific values from responses and save them to conversation memory
- Batch requests: Make multiple API calls by iterating over an array (requires
"fireAndForget": true) - Fire and forget: Optional asynchronous calls that don't wait for a response
- Caller identity: Call an API as the signed-in user with
${caller:token}— see Calling as the signed-in user
Since 6.2.0.
A header can reference the authenticated caller, so the agent calls an API with that user's credentials rather than one static credential baked into the config:
| Reference | Resolves to |
|---|---|
${caller:token} |
The caller's raw bearer token |
${caller:userId} |
The caller's principal name (not a secret) |
"headers": {
"Authorization": "Bearer ${caller:token}"
}This matters most when the API being called is EDDI's own. A static
credential is the wrong shape there: an OIDC token expires within the hour,
cannot be least-privilege, and collapses every action to a single synthetic
principal in the audit trail. With ${caller:token}, authorization stays EDDI's
normal per-endpoint enforcement and the audit trail names a real person. This is
what the EDDI-Manager Platform Operator uses.
Resolution is deliberately narrow, and each rule fails the call loudly rather than degrading quietly:
-
Same origin, or EDDI itself. The token is released only when the call targets the exact
scheme://host:portthe caller addressed, or this deployment's own address. The caller's origin is read from the inbound request; the deployment's own address comes from deployment configuration alone —eddi.self.base-url, otherwisehttp://127.0.0.1:${quarkus.http.port}(or thequarkus.http.hostaddress, when the listener binds one specific non-loopback address and127.0.0.1is therefore not this process) — never from an agent config or a request header. So a config naming a third-party host cannot exfiltrate a user's token, and no allow-list is needed for this to be safe by default. Withquarkus.http.port=0(a random port) and noeddi.self.base-url, the self address is unresolved: nothing is derived, and the token is released to the caller's origin only — seteddi.self.base-urlor a fixed port for the self release to apply.The second half exists for the Platform Operator, whose tools must call EDDI at an address the server can reach, which behind a tunnel, a port mapping or a reverse proxy is not the address the browser used. Be aware of what it means: the self address bypasses whatever sits in front of EDDI, so any agent that targets it with
${caller:token}reaches exactly what EDDI's own authorization allows that user, not what the proxy would also permit. If a deployment relies on its proxy for path or network restrictions on EDDI's API, seteddi.caller-identity.self-release.enabled=false. A turn whose caller origin could not be captured never gets the self release.With
eddi.security.ssrf-protection.enabled=true, the loopback default is refused before any request is made (loopback is exactly what that protection blocks), so such a deployment must seteddi.self.base-urlto an address the server can reach and that passes the full SSRF target policy — which also rejects private and link-local addresses, so an in-cluster service name that resolves to a private IP stays blocked.eddi.self.base-urlmust be a barescheme://host[:port]; a value with a path, query, fragment or credentials is ignored (logged at ERROR). A plain-httpvalue naming a non-loopback host is accepted but logged at WARN, since the caller's token would cross the network unencrypted unless a mesh or network layer protects it. -
Headers only.
${caller:token}in a query parameter, request body or request path is rejected. Tokens in URLs leak through access logs, proxies and browser history, and nothing outside a header is substituted anyway — a reference there would travel to the API as literal text.${caller:userId}may be used in headers and query parameters. -
Authenticated turns only. The identity comes from the request that drove the turn, so scheduled jobs and triggers cannot satisfy
${caller:token}. -
Fails closed. An unsatisfiable reference raises an error instead of resolving to an empty string, which would send
Bearerand surface later as a confusing401.
The resolved token is never written to conversation memory: authorization headers are scrubbed before the request is recorded.
Set eddi.caller-identity.enabled=false to forbid the feature outright.
The same reference works in an MCP server's apiKey, so a tool call reaches
that server as the chatting user rather than as a standing service principal.
Only the tool call carries the caller — the handshake and tools/list do not,
because the client is cached and a session opened with one user's token would be
reused by everyone after them. See
mcp-server.md.
The origin is taken from the inbound request as EDDI sees it. Behind a
TLS-terminating proxy or ingress, that is the internal hop — something like
http://10.0.0.5:8080 — while the caller addressed https://eddi.example. The
two do not match, so ${caller:token} fails closed and the agent reports that
the call targets a different origin, with nothing obviously wrong in the config.
If EDDI runs behind a proxy, enable forwarded-header handling so the request reflects what the caller actually addressed:
quarkus.http.proxy.proxy-address-forwarding=true
quarkus.http.proxy.enable-forwarded-host=trueThis is deliberately not on by default. It makes EDDI trust X-Forwarded-*
headers, which any client can send — safe when a trusted proxy overwrites them,
wrong when EDDI is directly reachable. Turn it on only together with a proxy
that sets those headers itself.
In this article we will talk about EDDI's httpCalls feature (calling other JSON APIs).
The httpCalls feature allows a Agent to consume 3rd party APIs and use the JSON response in another httpCall (for authentication or requesting a token for instance) or directly print the results in Agent's Output, this means, for example, you can call a weather API and use the JSON response in your Agent's output if the user asks about today's weather or the week's forecast!
We will emphasize the httpCall model and go through an example step by step, you can also download the example in Postman collection format and run the steps.
{
"targetServerUrl": "string",
"httpCalls": [
{
"name": "string",
"saveResponse": boolean,
"fireAndForget": boolean,
"responseObjectName": "string",
"actions": [
"string"
],
"preRequest": {
"batchRequests": {
"pathToTargetArray": "string",
"iterationObjectName": "string"
}
},
"request": {
"path": "string",
"headers": {},
"queryParams": {},
"method": "string",
"contentType": "string",
"body": "string"
},
"postResponse": {
"qrBuildInstructions": [
{
"pathToTargetArray": "String",
"iterationObjectName": "String",
"quickReplyValue": "String",
"quickReplyExpressions": "String"
}
],
"propertyInstructions": [
{
"name": "string",
"valueString": "string",
"scope": "string",
"fromObjectPath": "savedObjName.something.something",
"override": boolean,
"httpCodeValidator": {
"runOnHttpCode": [
<array of Integers>
],
"skipOnHttpCode": [
<array of Integers>
]
}
}
]
}
}
]
}An httpCall is mainly composed from the targetServer array of httpCalls, the latter will have request where you put all details about your actual http request (method,path,headers, etc..) and postResponse where you can define what happens after the httpCall has been executed and a response has been received; such as quick replies by using qrBuildInstruction.
You can use {memory.current.httpCalls.<responseObjectName>} to access your JSON object, so you can use it in output templating or in another httpCall, for example an httpCall will get the oAuth token and another httpCall will use in the http headers to authenticate to an API.
| Element | Description |
|---|---|
| targetServerUrl | (String) root/context path of the httpCall (e.g http://example.com/api) |
| httpCall.saveResponse | (Boolean) whether to save the JSON response into {memory.current.httpCalls} |
| httpCall.fireAndForget | (Boolean) whether to execute the request without waiting for a response to be returned, (useful for POST) |
| httpCall.responseObjectName | (String) name of the JSON object so it can be accessed from other httpCalls or outputsets. |
| httpCall.responseHeaderObjectName | (String) name under which the RESPONSE headers are stored, reachable as {memory.current.httpCalls.<responseHeaderObjectName>.<Header-Name>} and, for an LLM tool, returned under the result's headers key. Unset by default — set it only when the answer you need is in a header (a 201's Location, say) rather than the body, since headers reach conversation memory unredacted. Header names are matched case-insensitively, and credential-bearing headers (Set-Cookie, WWW-Authenticate, …) are never stored. |
| httpCall.actions | (String) name of the output/behavior set mapped to this http call. |
| httpCall.preRequest.batchRequests.pathToTargetArray | (String) JSON path to the target array to be used as body of requests e.g: "memory.current.output". Only honoured when fireAndForget is true; on a call that waits for a response the batch instruction is ignored and exactly one request is sent. |
| httpCall.preRequest.batchRequests.iterationObjectName | (String) name of the variable to be used for each element of array found in pathToTargetArray |
| httpCall.preRequest.batchRequests.maxBatchSize | (Integer, default 100, at most 1000 — both operator-configurable, see eddi.httpcalls.batch.* in the configuration reference) most requests the batch may expand into. A target array with more elements refuses the whole call — nothing is sent and the turn fails with a message naming the limit — instead of silently dropping the tail. Saving a value above the ceiling is refused with 400. |
| httpCall.request.path | (String) path in the targetServer of the httpCall (e.g /books) |
| httpCall.request.headers | (Array:<key, value> ) for each httpCall HTTP header |
| httpCall.request.queryParams | (Array: <key, value>) for each httpCall query parameter |
| httpCall.request.method | (String) HTTP Method of the httpCall (e.g GET,POST,etc...) |
| httpCall.request.contentType | (String) value of the contentType HTTP header of the httpCall |
| httpCall.request.body | (String) an escaped JSON object that goes in the HTTP Request body if needed. |
| httpCall.postResponse.qrBuildInstructions[].pathToTargetArray | (String) path to the array in your JSON response data. |
| httpCall.postResponse.qrBuildInstructions[].iterationObjectName | (String) a variable name that will point to the TargetArray. |
| httpCall.postResponse.qrBuildInstructions[].quickReplyValue | (String) Qute expression to use as a quickReply value. |
| httpCall.postResponse.qrBuildInstructions[].quickReplyExpressions | (String) expression to retrieve a property from iterationObjectName. |
| httpCall.postResponse.propertyInstructions.name | (String) name of property to be used in templating |
| httpCall.postResponse.propertyInstructions.valueString | (String) a static value can be set here if fromObjectPath is not defined. Typed siblings exist for other value types: valueInt, valueFloat, valueBoolean, valueObject, valueList. |
| httpCall.postResponse.propertyInstructions.scope | (
|
| httpCall.postResponse.propertyInstructions.fromObjectPath | (String) JSON path to the saved object e.g savedObjName.something.something. The value found is response data and is never rendered as a template (only valueString is). A string is stored as found — or, with convertToObject and a value shaped like a JSON object ({...}), parsed into an object. Any other value (number, boolean, JSON object or array) is stored as an empty string |
| httpCall.postResponse.propertyInstructions.override | (Boolean) flag for override |
| httpCall.postResponse.propertyInstructions.httpCodeValidator.runOnHttpCode | (Array: <Integer> ) a list of http code that enables this property instruction e.g [200] |
| httpCall.postResponse.propertyInstructions.httpCodeValidator.skipOnHttpCode | (Array: <Integer>) list of http code that enables this property instruction e.g [500,501,400] |
| HTTP Method | API Endpoint | Request Body | Response |
|---|---|---|---|
| POST | /apicallstore/apicalls |
http-call-model | N/A |
| GET | /apicallstore/apicalls/descriptors |
N/A | list of references to http-call-model |
| DELETE | /apicallstore/apicalls/{id} |
N/A | N/A |
| GET | /apicallstore/apicalls/{id} |
N/A | http-call-model |
| PUT | /apicallstore/apicalls/{id} |
http-call-model | N/A |
| GET | /apicallstore/apicalls/{id}/currentversion |
N/A | current version number (text/plain integer) |
| POST | /apicallstore/apicalls/{id}/currentversion |
N/A | 303 redirect to /apicallstore/apicalls/{id}?version=<latest> |
{
"targetServerUrl": "https://api.agent-metrics.com/v1/messages",
"httpCalls": [
{
"name": "sendUserMessageToAnalytics",
"actions": [
"send_input_to_analytics"
],
"saveResponse": false,
"fireAndForget": true,
"request": {
"method": "post",
"queryParams": {
"token": "<token>"
},
"contentType": "application/json",
"body": "{\"text\": \"{memory.current.input}\",\"message_type\": \"incoming\",\"user_id\": \"{memory.current.userInfo.userId}\",\"platform\": \"eddi\"}"
}
},
{
"name": "sendAgentMessageToAnalytics",
"actions": [
"send_output_to_analytics"
],
"saveResponse": false,
"fireAndForget": true,
"preRequest": {
"batchRequests": {
"pathToTargetArray": "memory.current.output",
"iterationObjectName": "output"
}
},
"request": {
"method": "post",
"queryParams": {
"token": "<token>"
},
"contentType": "application/json",
"body": "{\"text\": \"{output}\",\"message_type\": \"outgoing\",\"user_id\": \"{memory.current.userInfo.userId}\",\"platform\": \"eddi\"}"
},
"postResponse": {
"propertyInstructions": [
{
"name": "nameOfPropertyToBeUsedInTemplating",
"valueString": "StaticValueHereIfFromObjectPathIsNotDefined",
"scope": "step",
"fromObjectPath": "savedObjName.something.something",
"override": true,
"httpCodeValidator": {
"runOnHttpCode": [
200
],
"skipOnHttpCode": [
0,
400,
401,
402,
403,
404,
409,
410,
500,
501,
502
]
}
}
],
"qrBuildInstructions": [
{
"pathToTargetArray": "savedObjName.data.topics",
"iterationObjectName": "topic",
"templateFilterExpression": "topic.subType != 'specialSubType'",
"quickReplyValue": "{topic.name}",
"quickReplyExpressions": "property(topic_id({topic.id}))"
}
]
}
}
]
}We will do a step by step example from scratch (Agent creation to a simple conversation that uses httpCall)
For the sake of simplicity we will use a free weather API to fetch weather of cities by their names (api.openweathermap.org).
More about regular dictionaries can be found here.
Request URL
POST http://localhost:7070/dictionarystore/dictionaries
Request Body
{
"words": [
{
"word": "weather",
"expressions": "trigger(current_weather)",
"frequency": 0
}
],
"phrases": [
{
"phrase": "what is the weather",
"expressions": "trigger(current_weather)"
},
{
"phrase": "whats the weather",
"expressions": "trigger(current_weather)"
}
]
}Response Body
no content
Response Code
201
The
Locationheader contains the resource URI, e.g.eddi://ai.labs.dictionary/dictionarystore/dictionaries/<id>?version=1
More about behaviorSets can be found in Behavior Rules
Request URL
POST http://localhost:7070/rulestore/rulesets
Response Body
no content
Response Code
201
Request Body
{
"behaviorGroups": [
{
"name": "",
"behaviorRules": [
{
"name": "Ask for City",
"actions": [
"ask_for_city"
],
"conditions": [
{
"type": "inputmatcher",
"configs": {
"expressions": "trigger(current_weather)"
}
}
]
},
{
"name": "Current Weather in City",
"actions": [
"current_weather_in_city"
],
"conditions": [
{
"type": "inputmatcher",
"configs": {
"expressions": "trigger(current_weather)",
"occurrence": "lastStep"
}
}
]
}
]
}
]
}The
Locationheader contains the resource URI, e.g.eddi://ai.labs.rules/rulestore/rulesets/<id>?version=1
Note that we can pass user input to the http call using {memory.current.input}
Request URL
POST http://localhost:7070/apicallstore/apicalls
Request Body
{
"targetServerUrl": "https://api.openweathermap.org/data/2.5/weather",
"httpCalls": [
{
"name": "currentWeather",
"saveResponse": true,
"responseObjectName": "currentWeather",
"actions": [
"current_weather_in_city"
],
"request": {
"path": "",
"headers": {},
"queryParams": {
"APPID": "c3366d78c7c0f76d63eb4cdf1384ddbf",
"units": "metric",
"q": "{memory.current.input}"
},
"method": "get",
"contentType": "",
"body": ""
}
}
]
}Response Body
no content
Response Code
201
The
Locationheader contains the resource URI, e.g.eddi://ai.labs.apicalls/apicallstore/apicalls/<id>?version=1
More about outputSet can be found Output Configuration.
Note When you set
"saveResponse" : trueinhttpCallthen you can use{memory.current.httpCalls.<responseObjectName>}to access the response data and use Qute ({#for}) to iterate overJSONarraysif you have them in yourJSONresponse.
Request URL
POST http://localhost:7070/outputstore/outputsets
Request Body
{
"outputSet": [
{
"action": "ask_for_city",
"timesOccurred": 0,
"outputs": [
{
"valueAlternatives": [
{
"type": "text",
"text": "Which City would you like to know the weather of?"
}
]
}
]
},
{
"action": "current_weather_in_city",
"timesOccurred": 0,
"outputs": [
{
"valueAlternatives": [
{
"type": "text",
"text": "The current weather situation of {memory.current.input} is {memory.current.httpCalls.currentWeather.weather[0].description} at {memory.current.httpCalls.currentWeather.main.temp} °C"
}
]
}
]
}
]
}Response Body
no content
Response Code
201
The
Locationheader contains the resource URI, e.g.eddi://ai.labs.output/outputstore/outputsets/<id>?version=1
More about packages can be found here.
Important Workflow note
ai.labs.httpcalls&ai.labs.outputmust come afterai.labs.behaviorin order of the package definitionai.labs.templatinghas to be afterai.labs.output
Request URL
POST http://localhost:7070/workflowstore/workflows
Request Body
{
"workflowSteps": [
{
"type": "eddi://ai.labs.parser",
"extensions": {
"dictionaries": [
{
"type": "eddi://ai.labs.parser.dictionaries.integer"
},
{
"type": "eddi://ai.labs.parser.dictionaries.decimal"
},
{
"type": "eddi://ai.labs.parser.dictionaries.punctuation"
},
{
"type": "eddi://ai.labs.parser.dictionaries.email"
},
{
"type": "eddi://ai.labs.parser.dictionaries.time"
},
{
"type": "eddi://ai.labs.parser.dictionaries.ordinalNumber"
},
{
"type": "eddi://ai.labs.parser.dictionaries.regular",
"config": {
"uri": "eddi://ai.labs.dictionary/dictionarystore/dictionaries/{{dictionary_id}}?version=1"
}
}
],
"corrections": [
{
"type": "eddi://ai.labs.parser.corrections.stemming",
"config": {
"language": "english",
"lookupIfKnown": "false"
}
},
{
"type": "eddi://ai.labs.parser.corrections.levenshtein",
"config": {
"distance": "2"
}
},
{
"type": "eddi://ai.labs.parser.corrections.mergedTerms"
}
]
},
"config": {}
},
{
"type": "eddi://ai.labs.behavior",
"config": {
"uri": "eddi://ai.labs.rules/rulestore/rulesets/{{behaviourset_id}}?version=1"
}
},
{
"type": "eddi://ai.labs.httpcalls",
"config": {
"uri": "eddi://ai.labs.apicalls/apicallstore/apicalls/{{httpcall_id}}?version=1"
}
},
{
"type": "eddi://ai.labs.output",
"config": {
"uri": "eddi://ai.labs.output/outputstore/outputsets/{{outputset_id}}?version=1"
}
},
{
"type": "eddi://ai.labs.templating",
"extensions": {},
"config": {}
}
]
}Response Body
no content
Response Code
201
The
Locationheader contains the resource URI, e.g.eddi://ai.labs.workflow/workflowstore/workflows/<id>?version=1
Request URL
POST http://localhost:7070/agentstore/agents
Request Body
{
"workflows": [
"eddi://ai.labs.workflow/workflowstore/workflows/{{package_id}}?version=1"
],
"channels": []
}Response Body
no content
Response Code
201
The
Locationheader contains the resource URI, e.g.eddi://ai.labs.agent/agentstore/agents/<id>?version=1
Request URL
POST http://localhost:7070/administration/production/deploy/**<agent_id>**?version=1&autoDeploy=true
Response Body
no content
Response Code
202
Request URL
POST http://localhost:7070/agents/**<agent_id>**/start?environment=**<env>**
Response Body
no content
Response Code
201
The
Locationheader contains the conversation URI.
Request URL
POST http://localhost:7070/agents/<conversation_id>?returnDetailed=false&returnCurrentStepOnly=true
Request Body
{
"input": "weather"
}Response Body
{
"agentId": "5af8b075ba31c023bcb9ef3b",
"agentVersion": 1,
"environment": "production",
"conversationState": "READY",
"redoCacheSize": 0,
"conversationSteps": [
{
"conversationStep": [
{
"key": "input:initial",
"value": "weather"
},
{
"key": "actions",
"value": [
"ask_for_city"
]
},
{
"key": "output:text:ask_for_city",
"value": "Which City would you like to know the weather of?"
}
],
"timestamp": 1526247548410
}
]
}Response Code
200
Request URL
POST http://localhost:7070/agents/<conversation_id>?returnDetailed=false&returnCurrentStepOnly=true
Request Body
{
"agentId": "5af8b075ba31c023bcb9ef3b",
"agentVersion": 1,
"environment": "production",
"conversationState": "READY",
"redoCacheSize": 0,
"conversationSteps": [
{
"conversationStep": [
{
"key": "input:initial",
"value": "Vienna"
},
{
"key": "actions",
"value": [
"current_weather_in_city"
]
},
{
"key": "output:text:current_weather_in_city",
"value": "The current weather situation of Vienna is clear sky at 17.68 °C"
}
],
"timestamp": 1526247618080
}
]
}Response Code
200
Run it yourself. The GitBook-hosted Postman collections that used to be linked here were lost in the migration. You do not need them: every request is shown inline above, and Postman can import EDDI's own spec directly — Import → Link →
<your-eddi-host>/openapi(http://localhost:7070/openapifor a local install). It is generated from the running build, so unlike a committed collection it cannot go out of date. The same spec is browsable at<your-eddi-host>/q/swagger-ui.