GUIDES

Connect tools

A tool is just an HTTP endpoint you own. The agent decides when to call it, Agentifys makes the request, and the result comes back into the conversation.

Talk is cheap. A tool is what lets the agent act: look up an order, create a ticket, refund a charge. You register an endpoint, describe what it does and what arguments it takes, and the agent calls it when the conversation calls for it.

Register a webhook tool

In your project, add a custom tool and point it at your endpoint. You give it a name, a plain description (the agent reads this to decide when to use it), a method and URL, and a JSON schema for its inputs. This is the full stored shape of a tool, with every default spelled out.

json
{
  "name": "lookup_order",
  "display_name": "Look up an order",
  "description": "Look up the status of a customer order by its id.",
  "enabled": true,
  "input_schema": {
    "type": "object",
    "properties": { "order_id": { "type": "string" } },
    "required": ["order_id"]
  },

  "executor_type": "webhook",
  "webhook_url": "https://api.acme.com/orders/lookup",
  "webhook_method": "POST",
  "webhook_headers": { "Authorization": "Bearer {{user.auth_token}}" },
  "webhook_params": { "tenant_id": "{{user.data.tenant_id}}" },

  "requires_confirmation": false,
  "collection_triggers": []
}
FieldTypeDescription
namerequiredstringThe identifier the model calls. Keep it to letters, digits, underscore and hyphen — the model providers reject anything else.
display_namerequiredstringHuman label, shown in the confirmation prompt and the logs.
descriptionrequiredstringWritten for the model. Say what it does and when to use it.
enabledbooleanDefault true. A disabled tool is not offered to the model at all.
input_schemaobjectJSON Schema for the arguments. Defaults to an empty object schema, which means the model sends nothing.
executor_typestringDefault "mock". Must be set to "webhook" for your endpoint to be called. "mcp" is set by MCP discovery, not by hand.
webhook_urlstringDefault "". Required alongside executor_type "webhook", and re-validated by the SSRF guard on every call.
webhook_methodstringDefault "POST". "GET" sends the arguments as a query string instead of a body.
webhook_headersobjectDefault {}. Merged over Content-Type: application/json. This is where credentials go: there is no separate auth config.
webhook_paramsobjectDefault {}. Fixed values merged into every call under the model’s arguments, which win on a key conflict.
requires_confirmationbooleanDefault false. True holds the call and emits confirmation_required instead of running it.
collection_triggersstring[]Default []. Phrases that start guided input collection for this tool. Empty falls back to keywords derived from the name.
executor_type defaults to "mock". A tool left on the default never reaches your server, however complete the rest of its config looks: Agentifys only calls your endpoint when executor_type is "webhook" and webhook_url is non-empty. Setting the URL but leaving the type alone is the quiet failure to watch for, because the agent still answers, using invented data.

What a mock tool returns

A mock tool is a prototyping aid. It makes no outbound request. It echoes the parameters declared in input_schema merged with whatever the model sent, adds a slice of the caller's profile, and marks the payload "mock": true. The values are invented, and the agent will present them to the user as if they were real.

json
{
  "success": true,
  "mock": true,
  "result": "Mock response from 'lookup_order' — connect a webhook for real data.",
  "params": { "order_id": "A-40912" },
  "user": { "id": "u_42", "name": "Alice Chen", "role": "admin" }
}
Any parameter the schema declares but the model omitted comes back as the literal string <name>. If you see that, or "mock": true, in a tool result, the tool is not wired to your endpoint yet.

What Agentifys sends

For every method except GET, Agentifys sends a JSON body with three top-level keys: tool, input, and user. The model's arguments are nested under input, not spread at the top level, so read body.input.order_id, not body.order_id.

json
// POST https://api.acme.com/orders/lookup
// Content-Type: application/json
// Idempotency-Key: 7f3a91c2:toolu_01H8
// Authorization: Bearer <resolved from webhook_headers>
{
  "tool": "lookup_order",
  "input": {
    "tenant_id": "acme",
    "order_id": "A-40912"
  },
  "user": {
    "id": "u_42",
    "name": "Alice Chen",
    "email": "alice@acme.com",
    "role": "admin",
    "company": "Acme",
    "data": { "tenant_id": "acme", "plan": "pro" }
  }
}
FieldTypeDescription
toolstringThe tool’s name, exactly as registered. Useful when several tools share one endpoint.
inputobjectYour resolved webhook_params merged under the arguments the model chose. On a key collision the model’s value wins.
userobjectThe resolved user context for whoever is chatting: id, name, email, role, company, data, and any other field you signed into the identity.
Two fields are deliberately stripped from user before the request goes out: auth_token and creds. Secrets reach your endpoint only where you put them yourself, through {{user.auth_token}} or {{user.creds.<name>}} in webhook_headers or webhook_params. They are never included in the body wholesale.

With webhook_method set to GET there is no body at all. The same merged object is serialized into the query string, which means every argument is visible in your access logs and in any proxy in between.

Return application/json with a 2xx status. Whatever you return is handed back to the agent verbatim as the tool result, so keep it tight and readable: the model reads it to write its reply. A response body that does not parse as JSON is treated as a failed call.

Signal a business-level failure with a 2xx and an error field in the JSON, not with a 4xx. A 4xx or 5xx status never reaches the agent: it is converted into TOOL_UNAVAILABLE and your response body is discarded. An HTTP 404 for “order not found” tells the user the tool is broken; a 200 with { "error": "order not found" } lets the agent recover and ask a follow-up.
json
// Also HTTP 200. A 4xx is a failed call, not a readable error.
{
  "error": "order not found",
  "hint": "Ask the customer to confirm the order id."
}

The delivery contract

Agentifys will retry your endpoint, and the rules depend on the method you configured. Read this before you point a tool at anything that writes.

FieldTypeDescription
Timeout15sPer attempt, covering connect and response. There is no separate connect timeout.
Attempts4 maxOne initial attempt plus up to three retries, with 1s, then 5s, then 15s of delay between them. A call that retries to exhaustion occupies the turn for roughly 21 seconds of waiting plus up to 60 seconds of request time.
Idempotency-KeyheaderSent on every attempt of one tool call, with the same value: the trace id and the model’s tool-call id joined by a colon, for example 7f3a91c2:toolu_01H8.
Redirectsblockedfollow_redirects is off. Any 3xx ends the call immediately with TOOL_UNAVAILABLE and the Location header in the detail. Return 200 from the URL you registered.

Whether a failure is retried depends on whether replaying it could double-apply a write. GET, HEAD and OPTIONS count as idempotent; POST, PUT, PATCH and DELETE do not.

FieldTypeDescription
Connect error / connect timeoutalways retriedThe request provably never reached you, so replaying it is safe for every method.
Read timeout (15s)GET, HEAD, OPTIONS onlyThe request was sent and the response never came, so a write may have committed. Not replayed for POST, PUT, PATCH or DELETE.
HTTP 5xxGET, HEAD, OPTIONS onlyAmbiguous for the same reason. A 500 on a POST fails the call on the first attempt.
HTTP 4xxnever retriedDeterministic. The call fails immediately, whatever the method.
Malformed / non-JSON responseGET, HEAD, OPTIONS onlyTreated like any other unclassified request error.
The Idempotency-Key is advisory. Agentifys sends it and keeps it stable across retries, but it does not deduplicate anything on your behalf and it does not inspect your response. If a replay must not double-apply, your endpoint has to store the key and return the first result.

When the attempts are exhausted the agent receives this object as the tool result, and the conversation continues with the model knowing the tool failed. Agentifys also writes an error entry to the project logs, tagged webhook_failure, and emails a failure alert to the platform's configured alert address.

json
{
  "error": "TOOL_UNAVAILABLE",
  "message": "This tool is temporarily unavailable. Please try again later.",
  "detail": "HTTP 500 (attempt 1/4): upstream database timeout"
}

Template per-user values

webhook_headers and webhook_params support {{user.*}} templating, so a tool call can be scoped to whoever is chatting without the model ever seeing the raw secret. The values come from the user context you set (see Identify users). A webhook tool has no separate auth section: credentials go in a header, like any other header.

text
# webhook_headers — this is where credentials belong.
Authorization: Bearer {{user.auth_token}}

# From the per-user credential vault (decrypted at call time):
X-Api-Key: {{user.creds.stripe_key}}

# From the user context data you passed:
X-Tenant-Id: {{user.data.tenant_id}}

Templating resolves only in the values of webhook_headers and webhook_params. It does not resolve in webhook_url, and it does not resolve in the arguments the model produced.

{{user.creds.<name>}} pulls from the Fernet-encrypted vault and is substituted at request time. The raw secret is injected into your outbound call only, it never reaches the model. {{user.auth_token}} forwards the JWT you signed the identity with, and it is available only in tool scope, never in the system prompt.
An unresolved placeholder fails open. If a credential name is misspelled, the user has no such credential, or the identity was never verified, every leftover {{user.*}} token is replaced with an empty string and the request is sent anyway — so your endpoint receives a bare Authorization: Bearer and answers 401. That 401 is a 4xx, so it is not retried, and the agent is told the tool is unavailable. MCP server auth behaves the opposite way: an unresolved template there refuses the call up front with TOOL_DISABLED and names the missing credential.
{{user.auth_token}} is always empty in a scheduled run. The scheduler snapshots only name, email, role and company alongside the user id, so {{user.auth_token}} and every {{user.data.*}} field resolve to an empty string and the tool sends an empty credential. Vault credentials are the exception: {{user.creds.<name>}} is hydrated normally for scheduled runs. If a tool must work unattended, authenticate it with a vault credential or a static header, not with a forwarded session token.

The SSRF guard

Because tools make outbound HTTP from Agentifys's servers, every webhook URL is validated on every call, not only when you save it. The URL must use https. Its hostname is resolved, and the call is refused if any resulting address is private, loopback, link-local, reserved, multicast or unspecified — which includes the cloud metadata address 169.254.169.254. Re-validating per call is what limits DNS rebinding, and refusing to follow redirects is what stops a 3xx from pivoting your credential to an unchecked host.

A blocked URL returns TOOL_UNAVAILABLE with "This tool is misconfigured and was blocked for safety." before any request is made, so the credential in your headers never leaves the server.

If your endpoint lives inside a private network, expose it through a proper gateway or tunnel with a public hostname and a valid certificate. A URL that resolves to a private range will be blocked, and plain http is rejected outright.

Gate write actions

Reads can run freely. Writes should pause. Set requires_confirmation on a tool and the agent will not fire it silently: the call is held, the turn ends, and the widget surfaces an Approve / Cancel confirmation showing the user exactly what is about to happen and with what inputs. The next turn either runs the held call or drops it.

On the API this arrives as a confirmation_required event in the stream, carrying the tool name, its display_name, the input the model chose, and the two literal tokens you send back to resolve it. Nothing runs until the user approves.

json
// SSE event on a write tool awaiting approval
{
  "type": "confirmation_required",
  "id": "toolu_01H8",
  "name": "refund_charge",
  "display_name": "Refund a charge",
  "input": { "charge_id": "ch_9921", "amount": 4200 },
  "confirm": "__era_confirm__",
  "decline": "__era_decline__"
}
The widget renders the Approve and Cancel buttons for you. If you drive the stream yourself, send the confirm or decline token back as the next user message. Combined with the budget hard stop and per-user throttle, this keeps an agent that can act from acting recklessly.