Skip to main content

HTTP Request Bodies & Content Types

The HTTP Request skill can send a body with any method that supports one (POST, PUT, PATCH and DELETE). What you have to write in the configuration depends on two choices:

  • The body typeStatic Body, Dynamic Body or AI-Generated Body.
  • The content type — the media type the target endpoint expects.

This page is the reference for combining the two: what a good body description looks like for each content type, and what the platform sends on the wire.

Body types

A body is only sent when Include request body is enabled. With the switch off, an empty body is sent. When it is on, choose one of three body types:

Body typeWho writes the payloadUse it when
Static BodyYou. The value is sent exactly as configured, with no model involvement and no Liquid substitution.The payload never changes.
Dynamic BodyBoth. You provide the template, and the tokens inside it are resolved from the execution context or by the agent.You want the structure guaranteed and only some values to vary. This is the safest option for strict APIs.
AI-Generated BodyThe agent. You provide a description of the payload; the agent returns the whole body on each execution.The payload depends on the conversation, for example fields extracted from what the user asked for.

Body options

Dynamic vs. AI-Generated

Prefer Dynamic Body whenever you already know the exact shape of the payload. The agent is then only responsible for the individual values, so it cannot invent, rename or omit fields, and the values it does write are escaped for you. Reserve AI-Generated Body for payloads whose shape genuinely varies between executions.

How the tokens of a dynamic body are resolved, and how to write the descriptions the agent follows, is covered in AI-Generated Skill Parameters.

A static body is not a template

Liquid tokens are resolved in dynamic bodies, in the URL and in header values, but not in a static body: a {{token}} there travels to the endpoint as literal text. If any part of the payload has to vary, use a dynamic body.

Legacy bodies

Skills configured before body types were introduced report a Legacy body, and the designer offers to migrate them with Choose body type or Use empty body. A legacy skill keeps working: it sends the configured body if there is one, and otherwise lets the agent supply it, treating the body as optional. New configurations should use one of the three types above.

Headers and content type

The content type of the request is taken from the Content-Type header you configure on the skill. When you do not set one, the body is sent as application/json, so JSON payloads need no header at all.

Only these content types are accepted, because they are the ones the skill knows how to build content for:

Content typeWhat the body must contain
application/jsonA valid JSON document (the default when no header is set)
application/vnd.api+jsonA valid JSON:API document
application/json-patch+jsonA valid JSON Patch document
application/merge-patch+jsonA valid JSON Merge Patch document
application/ld+jsonA valid JSON-LD document
application/hal+jsonA valid HAL+JSON document
application/xmlAn XML document, sent as it is written
text/plainPlain text, sent as it is written
application/x-www-form-urlencodedURL-encoded key/value pairs (key1=value1&key2=value2), sent as they are written
multipart/form-dataA valid JSON document, whose properties are turned into form fields before the request is sent
Write the value exactly

The header value is matched against that list as an exact, lower-case string. application/json; charset=utf-8, Application/JSON or any other media type is rejected when you save the skill. The charset is added for you: text bodies are always encoded as UTF-8.

Headers are configured with Add Header as key/value pairs. Duplicate keys are allowed where an API requires them, and header values can contain Liquid tokens (keys can too, but keeping them static is far easier to reason about).

The following headers are managed by the platform, or unsafe to override, and are not accepted:

Authorization (use the Authentication section instead), Cache-Control, Connection, Cookie, Forwarded, Pragma, Proxy-Authorization, Proxy-Connection, Set-Cookie, Upgrade, User-Agent, Via, X-Forwarded-For, X-Real-IP, and any header whose name starts with CF.

Add headers

note

Two more rules apply to the URL rather than the body: it must use HTTPS, and when its path ends in a file extension, that extension must be one of .txt, .md, .html, .json, .xml, .pdf, .xlsx, .xls, .csv, .jpg, .png, .gif, .webp.

You do not have to explain the format to the agent

For an AI-Generated Body, the platform builds the description of the body argument itself: it states that the whole body must be returned as a single string, adds the format expected for the configured content type (for example "The string must contain a valid JSON document"), and then appends your instructions. So write about what the payload must contain, not about which syntax to emit.

The format line is the one thing you may still have to say yourself: when the Content-Type header value is a Liquid token, its value is unknown at design time, so no format hint can be added. In that case, state the expected format in your description.

JSON (application/json and the other JSON media types)

The default. Describe the payload as a JSON object.

Static Body — the exact JSON to send. It is sent verbatim: Liquid tokens are not resolved in a static body, so use a dynamic body if any part of the payload has to vary.

Static body
{
"source": "serenity-agent",
"notify": true
}

AI-Generated Body — describe every field, then give one complete example. The agent produces the JSON document on each execution:

Body description
Send the task to create, with these fields:
- title (string, required): short summary of the task.
- dueDate (string, optional): ISO-8601 date (YYYY-MM-DD). Resolve relative dates such as
"next Friday" against today's date. Send null when no deadline is mentioned.
- priority (string, required): one of "low", "medium", "high". Use "medium" when the user
does not say.
- tags (array of strings, optional): send an empty array when there are none.

Example:
{ "title": "Renew the SSL certificate", "dueDate": "2026-03-14", "priority": "high", "tags": ["infra"] }

Dynamic Body — provide the template and let the tokens be resolved. Keep the quotes that JSON requires around string values:

Dynamic body template
{
"title": "{{title}}",
"dueDate": "{{dueDate}}",
"priority": "{{priority}}",
"customerId": "{{customerId}}"
}

String values are JSON-escaped as they are substituted, so a value containing quotes, backslashes or line breaks cannot break the document.

The payload is validated before it is sent

For every JSON media type the body is parsed before the request goes out. An invalid document is reported as a configuration error for a Static or Dynamic body, and as an argument error for an AI-Generated one, which the agent sees and can correct on a new attempt.

tip

A Dynamic Body is the most reliable option for JSON APIs with a fixed contract: the field names, nesting and types come from your template, and the agent only fills in the values.

text/plain

Add a Content-Type: text/plain header. The body is sent as raw text, exactly as produced and with no validation, so do not describe it as JSON.

AI-Generated Body — say what the text must contain and how it must be formatted:

Body description
One line per incident, formatted as:
severity|component|one-sentence summary
Severity is one of P1, P2 or P3. Do not add a header line and do not use markdown.

Example:
P2|checkout-api|Checkout returns 500 for guest users since 14:20 UTC.

Dynamic Body — a text template with tokens works the same way:

Dynamic body template
{{severity}}|{{component}}|{{summary}}

application/xml

Add a Content-Type: application/xml header. As with plain text, the body is sent as raw XML and is not validated by the platform.

Because XML is easy to get subtly wrong, prefer a Dynamic Body with a fixed document and tokens for the values:

Dynamic body template
<ticket>
<subject>{{subject}}</subject>
<priority>{{priority}}</priority>
<requester>{{requesterEmail}}</requester>
</ticket>

If the document shape really does vary, use an AI-Generated Body and include a complete example document in the description, saying whether the API needs an XML declaration.

application/x-www-form-urlencoded

Add a Content-Type: application/x-www-form-urlencoded header.

The body is sent verbatim, so it must already be the encoded pair string. There is no JSON-to-form conversion for this content type: a JSON object would travel as raw JSON and the endpoint would reject it.

AI-Generated Body — describe the fields and show the encoded shape. The platform already tells the agent that the string must contain URL-encoded key/value pairs, so your job is to list the fields and their values:

Body description
Send these form fields:
- grant_type: always "client_credentials".
- scope (optional): the scopes the user asked for, separated by a plus sign.

Percent-encode any character that is not alphanumeric.

Example:
grant_type=client_credentials&scope=orders.read+orders.write

Static Body or Dynamic Body — paste the encoded pairs exactly as the API expects them:

Dynamic body template
grant_type=client_credentials&scope={{scope}}&customer={{customerId}}

Notes:

  • There is no nesting. Form encoding has no standard representation for objects or arrays; flatten them into individual fields (address.city, tags[0]) if the API expects that convention.
  • Values must be percent-encoded by whoever writes the string, including in a dynamic template: a value with a space, & or = will otherwise corrupt the pairs.

URL-encoded endpoint

multipart/form-data

Add a Content-Type: multipart/form-data header. Put only the media type there: the delimiter that separates the parts of the body (the boundary) is generated and appended for you, and a hand-written one would not match the body that is actually sent.

This is the one content type built from a JSON document: describe the body as JSON, and each property becomes one form field.

Body description
Send the document to register, with these fields:
- title (string, required): title of the document.
- category (string, required): one of "invoice", "contract", "report".
- notes (string, optional): send an empty string when the user gives no notes.

Example:
{ "title": "March invoice", "category": "invoice", "notes": "" }

The example above is sent as three form fields, title, category and notes.

Notes:

  • Field names come from the JSON keys, so they must match what the endpoint expects, casing included.
  • Nesting is flattened for you. A nested object becomes a dotted field name and an array becomes an indexed one: {"customer":{"id":"12"},"tags":["infra"]} is sent as the fields customer.id and tags[0]. Only use nesting when your endpoint actually reads those names.
  • Values are stringified: numbers and booleans are sent as their literal text, and null becomes an empty field.
  • The JSON must parse. An invalid document is rejected before the request is sent (a trailing comma is tolerated).
  • File uploads are not built this way; the fields are text parts.

Multipart-encoded endpoint

Checklist before saving the skill

  • The Content-Type header is one of the accepted values, in lower case and with no extra parameters (or absent, for JSON).
  • The body description says what the payload must contain, and includes one complete example in the shape that content type expects.
  • Optional fields are marked as optional, with an explicit instruction for the "unknown value" case.
  • Anything that must never change lives in a Static or Dynamic body, not in a description. Remember that a static body gets no Liquid substitution.
  • The skill description explains when the agent should call the endpoint, not just what the payload looks like.
  • The agent's system definition provides the context the parameters need (today's date, current user, tenant, language).