Skip to main content

Daemon

How to invoke an agent from a Salesforce Flow or over HTTP, 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.

There are two ways in, and they reach the same capability:

  • From a Salesforce Flow, using the Invoke Agent (Daemon) action. No code.
  • Over HTTP, from a cron job, a Lambda, an automation tool, or an Apex callout.

Two conversation modes are supported either way:

  • 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: The Daemon has a 120-second timeout. For tasks that may take longer, consider breaking the work into smaller steps.


From a Salesforce Flow

In Flow Builder, add an Action element and search for:

Invoke Agent (Daemon)

It appears under the Flourish Agent Platform category.

Inputs

InputRequiredDescription
Agent External IdThe Agent ID of the agent to invoke. Copy this from the Agent Builder using the Copy Agent ID button.
MessageThe message to send to the agent: the equivalent of what a user would type in chat.
Stateful-Set to true to start or continue a persistent conversation. Defaults to false.
Conversation Id-Required when resuming a stateful conversation. Pass the Conversation ID returned by a previous Daemon call. Leave blank to start a new conversation.
Host Context JSON-An optional JSON string of additional context passed to the agent at runtime. The agent can reference these values in its reasoning.
Group Key-Names which memory the agent uses for this call. See Choosing which memory to use. Agents set to No memory ignore it.
Idempotency Key-Stops a duplicate call starting a second turn while the first is still running. See Stopping duplicate calls.
Additional Instructions-Extra instructions for this one call, applied on top of the agent's profile. Use it for something specific to this Flow that does not belong in the agent itself, such as an output format.

Outputs

OutputDescription
ResponseThe agent's text response.
Conversation IdThe conversation ID for stateful calls. Store this in a Flow variable if you need to resume the conversation later.

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:

Group Key = {!$Record.AccountId}
Group Key = {!$Record.CaseNumber}

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-1 never 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.


Stopping duplicate calls

Every call costs money and can take up to two minutes. If the same call arrives twice while the first is still running, whether from a Flow 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.

Idempotency Key closes that. Set it to a value that identifies the request rather than the attempt, and a duplicate arriving while the first call is still in flight is refused instead of started:

Idempotency Key = {!$Record.Id}
Idempotency Key = {!$Record.CaseNumber}

The refusal comes back as a fault whose message contains 409 and says a call with that key is already running. Handle it by doing nothing: the first call is still working, and its result is the one you want.

Use the record id, not a formula that changes between attempts. A key that is unique per attempt is the same as no key at all.


When a Flow call fails

The Daemon raises a Flow fault on any error, and the message always starts with the HTTP status, so a fault path can branch on it with CONTAINS.

Three of them mean do not retry, because retrying makes each one worse:

In the messageWhat happenedWhat to do
402The org has reached its monthly spend limit for agent calls.Nothing clears until the limit resets at the start of the next month, UTC. Raise the cap or wait. A retry loop just burns Flow interviews.
504The agent ran out of time on this turn.Split the work into smaller calls. A turn heavy enough to time out will time out again.
409A call with this Idempotency Key is already running.Nothing. Wait for the first call and use its result.

401 or 403 means the org credentials are being refused, and 404 means no agent with that External Id is visible to this org. Both are configuration rather than something a retry fixes.


Over HTTP

The Flow action wraps a REST endpoint. Call it directly when the caller isn't Salesforce, or when you want one round trip from your own code.

POST https://agentplatform.toflourish.org/api/agents/{agent_id}/invoke
Content-Type: application/json

{agent_id} is the same Agent ID the Flow action takes, from Copy Agent ID in the Agent Builder.

note

The Agent Platform has its own host, agentplatform.toflourish.org. It is not on api.toflourish.org, which serves other Flourish products.

Headers

HeaderRequiredPurpose
fl-api-orgIdentifies your organization.
fl-api-tokenYour API token.
fl-api-envThe environment to run against, such as prod or dev.
Idempotency-KeyIf you retryA 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

FieldTypePurpose
messagestringThe message to send. Equivalent to the Flow action's Message.
statefulbooleantrue to start or continue a remembered conversation. Defaults to false.
conversation_idstring | nullThe ID from a previous stateful call. null starts a new conversation.
host_contextobject | nullStructured data passed alongside the message for the agent to reason over.
additional_instructionsstring | nullExtra instructions for this call only, layered on the agent's configured prompt.
instance_idstring | nullAddresses a specific child instance of an agent configured per instance. Required for those agents, ignored for all others.
{
"message": "What are the top 5 accounts by revenue?",
"stateful": false,
"conversation_id": null,
"host_context": null,
"additional_instructions": null,
"instance_id": null
}

The Flow inputs map onto these directly, which is worth knowing if you're moving an automation from Flow to code:

Flow inputJSON field
Agent External Idin the URL, as {agent_id}
Messagemessage
Statefulstateful
Conversation Idconversation_id
Host Context JSONhost_context

Response

Returns 200 with:

FieldTypeMeaning
responsestringThe agent's text reply.
conversation_idstring | nullThe conversation to pass back on the next stateful call.
terminated_reasonstring | nullnull on a normal finish. Otherwise max_iterations, loop_detected, or llm_error.
blockedbooleantrue when the agent's safety policy stopped the reply, in which case response is a placeholder.
{
"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,
"blocked": false
}

Check terminated_reason and blocked 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. This matters most when one agent consults another: without the check, a degraded reply gets relayed onward as though it were authoritative.

Example request

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"
}
}'

Errors

StatusMeaningWhat to do
403The token lacks chat permission on this agent.Fix the grant. Retrying won't help.
409A request with the same Idempotency-Key is already running.Treat it as already in flight and wait for the original.
504The turn passed the 120-second limit and the server cancelled it.Don't retry. See below.

Retries and idempotency

A turn is capped at 120 seconds. On overrun the server cancels it and returns 504.

Don't auto-retry a 504. The work was cancelled 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.

If your caller retries at all, send an Idempotency-Key. Use a stable value tied to the logical request, such as the source record ID. A concurrent duplicate carrying the same key gets 409 rather than starting a second turn.

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.

warning

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.

What the HTTP path doesn't do

  • No file uploads. Attaching files to a conversation needs a chat session.
  • 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 response as :::action ... ::: blocks for you to parse, not returned as structured fields.

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.

Leave Stateful unchecked (or set stateful to false) and leave the Conversation ID blank.

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 blank. The output 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 on a Salesforce record or in a Flow variable so it survives across separate Flow executions, 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 is accepted. For example, you could pass record data so the agent can reason about a specific Salesforce record:

{
"record_id": "0013X00000AbCdEF",
"account_name": "Acme Corp",
"annual_revenue": 5000000,
"owner": "Jane Smith"
}

In Flow, build this string using a Text Template or an Assignment element that concatenates field values into a JSON structure. Over HTTP, send it as a JSON object in host_context.


Example Flow Patterns

Summarize a Case on Close

Trigger a Flow when a Case is closed. Pass the case description and comments to the agent, then write the summary back to a custom field.

  1. Trigger: Case status changes to Closed.
  2. Get Records: Retrieve the case details.
  3. Invoke Agent (Daemon) action: Send the case description and comments as the message.
  4. Update Records: Write the agent's response to a custom AI_Summary__c field on the Case.

Multi-Turn Lead Qualification

Use a stateful conversation to ask a prospect a series of questions over time, maintaining context across each step.

  1. Trigger: Lead is created.
  2. Invoke Agent (Daemon) action: Stateful, no Conversation ID (starts a new conversation).
  3. Update Records: Store the returned Conversation ID on the Lead record.
  4. Later trigger: Lead stage is updated.
  5. Invoke Agent (Daemon) action: Stateful, passing the stored Conversation ID. The agent continues the thread with full prior context.

Classify Incoming Emails

Trigger on new Email Message records. The agent classifies the intent and urgency, and the Flow routes accordingly.

  1. Trigger: Email Message is created.
  2. Invoke Agent (Daemon) action: Send the email body as the message.
  3. Decision: Parse the agent's response (e.g. "urgent", "billing", "general").
  4. Branch: Route to the appropriate queue or create a high-priority Case.

Tips

Keep messages focused. The clearer and more specific your message input, 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.

Store Conversation IDs on records. For long-running stateful workflows, persist the Conversation ID on a Salesforce field so it survives across separate Flow executions, batch jobs, or time-triggered automations.

Watch the timeout. Complex agent tasks, especially those involving multiple tool calls, large datasets, or code execution, can take significant time. If you're consistently hitting the 120-second limit, consider splitting the work across separate invocations or simplifying the agent's task scope.