Daemon
How to call an agent from your own code and get its answer back, with no chat interface involved.
Overview
The Daemon calls a Flourish agent and waits for its answer. The agent receives a message, reasons over it using the tools and trusted resources configured on its deployed version, and returns a text response.
You call it over HTTP, from a cron job, a Lambda, an automation tool, or an Apex callout. In Salesforce, a Flow action makes the same call with no code: see From a Flow.
Two conversation modes are supported:
- Stateless: each call is independent. No history is retained between invocations.
- Stateful: the agent maintains a conversation across calls using a Conversation ID. Use this for multi-turn automations where the agent needs to remember what was said previously.
Note: An agent's turn is limited to 240 seconds unless its configuration says otherwise. A caller that stops waiting sooner, as any Salesforce callout does at 120 seconds, never sees the answer to a longer turn. For tasks that may take longer, break the work into smaller steps. See How long a call can take.
Over HTTP
POST https://agentplatform.toflourish.org/api/agents/{agent_id}/invoke
Content-Type: application/json
{agent_id} is the agent's Agent ID. See Finding an agent's ID.
The Agent Platform has its own host, agentplatform.toflourish.org. It is not on api.toflourish.org, which serves other Flourish products.
Headers
| Header | Required | Purpose |
|---|---|---|
fl-api-org | ✅ | Identifies your organization. |
fl-api-token | ✅ | Your API token. |
fl-api-env | ✅ | The environment to run against, such as prod or dev. |
fl-api-user | To call as a person | A person's Agent Platform user name, sent with that person's own token. See Calling as a person. |
fl-share-key | To run an agent shared with you | A share key, with the three above or instead of them. See Running an agent shared with you. |
Idempotency-Key | If you retry | A stable value tied to the logical request. See Retries. |
See Authentication for the shared scheme. Your token also needs chat permission on the agent you're invoking, or the call returns 403.
Request body
| Field | Type | Purpose |
|---|---|---|
message | string | The message to send: the equivalent of what a user would type in chat. |
stateful | boolean | true to start or continue a remembered conversation. Defaults to false. |
conversation_id | string | null | The ID from a previous stateful call. null starts a new conversation. |
host_context | object | null | Structured data passed alongside the message for the agent to reason over. See Passing Host Context. |
additional_instructions | string | null | Extra instructions for this call only, layered on the agent's configured prompt. Use it for something specific to this caller that does not belong in the agent itself, such as an output format. |
group_key | string | null | Names which memory the agent uses for this call. See Choosing which memory to use. |
origin | string | "user", the default, for a message a person wrote. "host" for one your system sends, which the agent may choose not to answer. See A message from your system. |
client | string | null | Your integration's name, such as slack or case-summaries. See below. |
{
"message": "What are the top 5 accounts by revenue?",
"stateful": false,
"conversation_id": null,
"host_context": null,
"additional_instructions": null,
"group_key": null,
"origin": "user",
"client": null
}
client names your integration. A conversation keeps the name of whatever started it, and so does each message your integration sends into it, so Greenhouse can show the people reading a conversation where it and its messages came from. A call that continues a conversation leaves the name it started with. The name is a label and grants nothing. It takes up to 64 letters, digits and . _ : -, starting with a letter or digit. Name it when people will read the conversations your integration starts.
Response
Returns 200 with:
| Field | Type | Meaning |
|---|---|---|
response | string | The agent's text reply. Empty when responded is false. |
conversation_id | string | null | The conversation to pass back on the next stateful call. |
terminated_reason | string | null | null on a normal finish. Otherwise one of the reasons below. |
failure_category | string | null | On a provider_error, what kind of failure it was. See Sending a failed call again. |
nothing_ran | boolean | true when the turn failed before the agent did anything. See Sending a failed call again. |
blocked | boolean | true when the agent's safety policy stopped the reply, in which case response is a placeholder. |
compose_failed | boolean | true when the agent did its work but its reply could not be written. See below. |
responded | boolean | false only when the agent chose not to answer a message sent with origin set to "host". |
files | array | null | The files the agent delivered on this call. See Files the agent delivers. |
usage | object | What the call spent. See What a call cost. |
{
"response": "Here are the top 5 accounts by revenue:\n\n1. Acme Corp - $2.3M\n...",
"conversation_id": "uuid-if-stateful-new",
"terminated_reason": null,
"failure_category": null,
"nothing_ran": false,
"blocked": false,
"compose_failed": false,
"responded": true,
"files": null,
"usage": {
"tokens_in": 865,
"tokens_out": 192,
"cache_read_tokens": 47453,
"cache_write_tokens": 0,
"estimated_cost": 0.016791,
"cost_by_basis": {"billed": 0.016791}
}
}
Other fields can appear. Ignore any you don't use.
Check terminated_reason, blocked and compose_failed before trusting the text. A turn that hit max_iterations still returns whatever partial answer it had composed, and it reads like a complete one. A caller that writes it to a record or emails it to a customer has no other way to tell the difference. This matters most when one agent consults another: without the check, a degraded reply gets relayed onward as though it were authoritative.
When a reply is partial
The list of reasons grows, so treat any value you have not seen as a partial reply too.
terminated_reason | What happened | Retrying |
|---|---|---|
max_iterations | The agent needed more steps than one call allows. | Split the work into smaller calls. The same call stops in the same place. |
loop_detected | The agent repeated itself. | Same. Look at the instructions rather than calling again. |
turn_timeout | The agent ran out of time and answered early with what it had. | Split the work into smaller calls. |
llm_error | The model call failed. | Don't retry automatically. See below. |
provider_error | The agent's own AI provider, on its own key or a Flourish seat, failed, and the platform's retries did not get past it. | Depends on failure_category. See below. |
compose_failed is not one of those reasons, because the turn finished. The agent did its work and any tools it used ran, but the last step, writing the reply, failed. response is then the agent's own summary of what it did, or a short notice that the work happened, rather than an answer. Don't send the call again as though nothing happened: the work is done, and a second call does it again.
Sending a failed call again
A turn that ends in llm_error or provider_error says whether the same call is safe to send again, and whether it is likely to work.
nothing_ran is true when the turn failed before the agent did anything, so sending it again repeats no work. When it is false, the agent may already have used its tools, and a second call could do the same work twice.
failure_category says what kind of provider_error it was:
failure_category | What happened | Sending it again |
|---|---|---|
transient | The provider was overloaded or briefly down. | Likely to work after a pause. |
quota | The provider's rate limit. | Works once the limit recovers. |
credential | The provider refused the key. | Never works. Someone has to fix the agent's AI provider settings. |
other | Anything else. | Don't. |
Treat a value you have not seen as other.
Retry automatically only when nothing_ran is true and failure_category is transient, and wait a little first: the platform has already tried three times. Anything else goes to a person, or waits for the next scheduled run.
Example request
- curl
- Python
- Apex
curl -X POST https://agentplatform.toflourish.org/api/agents/YOUR_AGENT_ID/invoke \
-H "Content-Type: application/json" \
-H "fl-api-org: YOUR_ORG" \
-H "fl-api-token: YOUR_TOKEN" \
-H "fl-api-env: prod" \
-H "Idempotency-Key: case-0013X00000AbCdEF" \
-d '{
"message": "Summarize this case for the account team.",
"stateful": false,
"host_context": {
"record_id": "0013X00000AbCdEF",
"account_name": "Acme Corp"
}
}'
import requests
resp = requests.post(
"https://agentplatform.toflourish.org/api/agents/YOUR_AGENT_ID/invoke",
headers={
"fl-api-org": "YOUR_ORG",
"fl-api-token": "YOUR_TOKEN",
"fl-api-env": "prod",
"Idempotency-Key": "case-0013X00000AbCdEF",
},
json={
"message": "Summarize this case for the account team.",
"stateful": False,
"host_context": {
"record_id": "0013X00000AbCdEF",
"account_name": "Acme Corp",
},
},
timeout=420,
)
resp.raise_for_status()
data = resp.json()
if data["blocked"] or data["compose_failed"] or data["terminated_reason"]:
raise RuntimeError(f"Not a finished answer: {data['terminated_reason']}")
print(data["response"])
HttpRequest req = new HttpRequest();
req.setEndpoint('callout:Flourish_Agent_Platform/api/agents/'
+ agentId + '/invoke');
req.setMethod('POST');
req.setHeader('Content-Type', 'application/json');
req.setHeader('fl-api-org', orgId);
req.setHeader('fl-api-token', token);
req.setHeader('fl-api-env', 'prod');
req.setHeader('Idempotency-Key', caseId);
req.setTimeout(120000);
req.setBody(JSON.serialize(new Map<String, Object>{
'message' => 'Summarize this case for the account team.',
'stateful' => false,
'host_context' => new Map<String, Object>{ 'record_id' => caseId }
}));
HttpResponse res = new Http().send(req);
Map<String, Object> body =
(Map<String, Object>) JSON.deserializeUntyped(res.getBody());
String answer = (String) body.get('response');
Store the host and token in a Named Credential rather than in the class, so the token never sits in source control.
Errors
| Status | Meaning | What to do |
|---|---|---|
400 | The agent is set to Per person memory, and the call named no person. | Call as the person. See Calling as a person. |
401 | The credentials were refused. That includes a person's user name sent with a token that is not theirs, and a share key that was revoked or has expired. | Fix the headers. Retrying won't help. |
402 | A monthly spend limit has been reached, either the org's or this agent's own. | Terminal for the month. Don't retry: raise the limit, or wait for the reset at the start of the next month, UTC. The agent's own limit is in its Cost settings. |
403 | The token lacks chat permission on this agent. | Fix the grant. Retrying won't help. |
404 | No agent with that ID that you can reach. An agent in another organization that is not shared with you answers exactly as one that does not exist. A stateful call naming a conversation that is not this agent's gets it too. | Check the ID, and that the agent is shared with you. |
409 | A request with the same Idempotency-Key is already running. | Treat it as already in flight and wait for the original. |
429 | A conversation has passed 200 calls in 2 minutes. | Pause and retry. Only stateful callers reusing one conversation_id can reach it. |
504 | A step of the turn ran long past the agent's time limit, and the server canceled it. | Don't retry. See below. |
How long a call can take
A turn is limited by the agent's own turn limit, 240 seconds by default. At the limit the agent normally stops itself between steps and you get a 200 with the partial answer it composed and terminated_reason set to turn_timeout. Only when a single step runs more than two minutes past the limit is the turn canceled outright, with a 504 and nothing kept.
Set your client's read timeout to the limit plus about three minutes, seven minutes for an agent on the default, or the partial answer loses the race to your own timeout. The Python example above uses 420 seconds.
A timeout on your side, or a 500, does not mean nothing happened. The agent may have used its tools, or finished, after your client stopped waiting. Before calling again for anything that changes data, check whether the work was done.
Apex and Flow cannot wait that long. Salesforce caps any callout at 120 seconds, so a Salesforce caller sees a timeout on a turn that runs longer, while the agent carries on and the call is charged. Keep work that is called from Salesforce well inside two minutes.
A script calling in from a scheduled run has a tighter bound, unrelated to the turn: its connection is dropped after about 350 seconds with nothing passing, so a call that waits longer never receives its answer. Keep each such call under five minutes, or make several.
Retries and idempotency
Every call costs money and can take minutes. If the same call arrives twice while the first is still running, whether from a caller that retries on a fault, a screen someone double-clicks, or a scheduled job overlapping its previous run, you get two turns and two charges for one piece of work.
If your caller retries at all, send an Idempotency-Key. Use a value that identifies the request rather than the attempt, such as the source record ID. A concurrent duplicate carrying the same key gets 409 rather than starting a second turn. Handle it by doing nothing: the first call is still working, and its result is the one you want. A key that is unique per attempt is the same as no key at all.
Without the header, every request runs independently. That's deliberate, so bulk fan-out (many records through one prompt template) doesn't collide with itself.
Don't auto-retry a 504. The work was canceled for being too heavy, so an immediate retry runs the same heavy work again while the first attempt may still be winding down. For work that genuinely takes longer, use a chat session or a scheduled task instead.
A client that retries on its own timeout, against a stateful conversation that grows with each attempt, is the pattern that produces runaway cost. If you retry, send an idempotency key. Better still, acknowledge the request immediately and do the work asynchronously.
Files the agent delivers
When the agent hands over a file, such as a report or a spreadsheet, it is listed in files, which is null when there are none. A file from another agent this one consulted is listed too. Each entry carries:
| Field | Meaning |
|---|---|
filename | The file's name. |
content_type | Its type, such as text/csv. |
size | Its size in bytes. |
conversation_id | The conversation that holds it. |
The response carries no link. Ask for one with the same headers, URL-encoding the file name:
GET https://agentplatform.toflourish.org/api/conversations/{conversation_id}/outputs/{filename}
It returns a url to download the file from, good for an hour (expires_in is in seconds). Ask again whenever you need a fresh one. Use the conversation_id on the file's own entry: a stateless call returns no conversation ID at the top of the response, and its files are still there.
What a call cost
usage reports what the call spent, including any agent it consulted.
| Field | Meaning |
|---|---|
tokens_in | Fresh input only: what was neither read from nor written to the prompt cache. On its own it understates what the model read. |
tokens_out | What the model wrote. |
cache_read_tokens, cache_write_tokens | Input read from and written to the prompt cache. The model read tokens_in and both of these in all. |
estimated_cost | US dollars at the AI provider's list price, whoever pays. |
cost_by_basis | The same dollars split by who pays, listing only the parts that apply: billed through Flourish, seat on a Flourish seat, and customer on the agent's own AI provider key. |
Read cost_by_basis before treating estimated_cost as a charge. A seat amount is charged to no one; it is what the same work would have cost. A customer amount is billed by your own AI provider, not by Flourish. One call can carry more than one part.
What the HTTP path doesn't do
- No file uploads. Attaching files to a conversation needs a chat session. Files the agent delivers do come back; see Files the agent delivers.
- No progress updates. Anything the agent would stream as it works is discarded. You get the final response only.
- Actions arrive as text. If the agent is configured with actions, they're appended to
responseas:::action ... :::blocks for you to parse, not returned as structured fields.
Choosing which memory to use
An agent set to Shared across everyone does not have to keep a single memory for the whole org. group_key names which memory this call should use: a class, an account, a case, a household.
Nothing is set up in advance. The same name always reaches the same memory, and a name you have never used simply starts one. So the name can come straight from your own data, such as an account number or a case number.
It takes 1 to 128 characters, and allows letters, digits, and . _ - @. Anything else is rejected.
Two things worth knowing:
- It is scoped to your org automatically. Two orgs that both use
class-1never touch the same memory, and there is no way to name another org's group. - Agents set to No memory ignore it. There is no memory to address, so passing it changes nothing.
group_key and conversation_id answer different questions. conversation_id continues one specific thread. group_key chooses whose long-term memory the agent draws on. You can pass both.
Stateless vs. Stateful
Stateless (default)
Each invocation is completely independent. The agent has no memory of previous calls. Use this for one-shot tasks like summarizing a record, classifying text, or generating a draft email.
Set stateful to false and leave conversation_id empty.
Stateful
The agent maintains conversation history across calls. The first call starts a new conversation and returns a Conversation ID. Pass that ID back on the next call, and the agent picks up where it left off.
First call: Set stateful to true and leave conversation_id empty. The response includes a new Conversation ID (e.g. conv-abc-123).
Subsequent calls: Set stateful to true and pass the stored conversation_id. The agent continues with full context from earlier turns.
Store the Conversation ID somewhere that outlives the call, such as on the record the work is about, so it survives across separate runs, batch jobs, or time-triggered automations.
A stateful conversation grows with every turn, so it costs more per call as it lengthens. Start a fresh conversation when the topic changes rather than extending one indefinitely.
Passing Host Context
Host Context lets you pass structured data to the agent alongside the message. The agent receives this as additional context it can draw on when forming its response.
Any valid JSON object is accepted. For example, you could pass record data so the agent can reason about a specific record:
{
"record_id": "0013X00000AbCdEF",
"account_name": "Acme Corp",
"annual_revenue": 5000000,
"owner": "Jane Smith"
}
Send it as a JSON object in host_context.
A message from your system
Set origin to "host" when the message comes from your system rather than from a person, such as a scheduled check-in: Here are today's open cases. The agent reads it as a note from the system it works in, and it may decide there is nothing to say. Then responded is false, response is empty, and no reply is added to the conversation.
That leaves "stay quiet unless something changed" to the agent, rather than asking it for a reply every time and searching the reply for a sign that something should be passed on. Check responded before you send response anywhere.
With the default, "user", the agent always answers and responded is always true.
Calling as a person
A call acts as your organization unless it names a person. To act as one person, send their Agent Platform user name in fl-api-user and their own token in fl-api-token: the same user name and token they sign in to Greenhouse with, or enter in the My Access component in Salesforce.
fl-api-org: YOUR_ORG
fl-api-user: jsmith
fl-api-token: JSMITH_TOKEN
fl-api-env: prod
- An agent set to Per person memory needs it. Called as your organization, such an agent refuses with
400, since there is no person whose memory to use. Called as the person, it uses theirs. - The token has to be the person's. Sent with your organization's token instead, the call is refused with
401if the person has a token of their own, and otherwise runs as your organization. - The call can do what the person can. Without chat permission on the agent, it returns
403. - Someone who belongs to no organization leaves out
fl-api-organd sends onlyfl-api-userandfl-api-token, withfl-api-env.
Running an agent shared with you
Another organization can let yours run one of its agents and give you a share key for it (see Share Keys). Send the key in the fl-share-key header. It is a credential like your token: keep it on your server, never in a web page or a URL.
| What you send | What runs |
|---|---|
fl-share-key with your own fl-api-org, fl-api-token and fl-api-env | The shared agent, on your organization's data, billed to your organization. |
fl-share-key alone | The shared agent, as the organization it was shared with. |
Send your own headers as well when the agent should work with your organization's own systems, such as your Salesforce. The key decides what you can do with the agent; your headers decide whose data it works on and who pays.
A key that was revoked or has expired is refused with 401. An agent in another organization that you cannot reach answers 404, exactly as one that does not exist.
Tips
Keep messages focused. The clearer and more specific your message, the better the agent's response. If the task depends on record data, include the relevant fields in Host Context rather than embedding them as raw text in the message.
Use stateful mode for multi-step tasks. If you need the agent to accumulate information across several steps in a process, a stateful conversation lets it build up context naturally rather than re-establishing it every time.
Watch the timeout. Complex agent tasks, especially those involving multiple tool calls, large datasets, or code execution, can take significant time. Anything near your caller's own wait is a reason to split the work across separate invocations or narrow the agent's task.