Portable routines
Portable routines are routine definitions written as deterministic markdown. Use this surface when you keep routines as files, generate them from code, or need a stable text form for review. It’s also what the dashboard’s routine editor puts on your clipboard when you copy a routine as text, so a file exported from one agent is valid input to this API on another.
This is not draft assist. Draft assist turns free prose into a proposed routine with an LLM. Portable routine markdown is parsed by the grammar package and then validated as the normal structured routine definition — no model call, so the same input always parses to the same routine.
Endpoints
GET /api/v1/agents/{agentId}/routines/{routineId}/portable
PUT /api/v1/agents/{agentId}/routines/{routineId}/portable
POST /api/v1/agents/{agentId}/routines/portable
POST /api/v1/routines/portable/canonicalizeThese take a workspace token — call them against the host that issued it, see Workspaces and tokens. All four use this JSON envelope:
{
"grammarVersion": 1,
"content": "---\ngrammar: 1\nname: Refund check\ntrigger: customer asks for a refund\n---\nAsk for @order_id.\n-> end"
}Create returns the same canonical envelope plus routineId:
curl -sS -X POST https://api.radioso.ai/api/v1/agents/<agentId>/routines/portable \
-H "Authorization: Bearer $RADIOSO_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"grammarVersion":1,"content":"---\ngrammar: 1\nname: Refund check\ntrigger: customer asks for a refund\n---\nAsk for @order_id.\n-> end"}'Write a routine in markdown
A document is YAML-like frontmatter, then one routine line per paragraph:
---
grammar: 1
name: Refund check
trigger: customer asks for a refund
reentry: once_per_conversation
priority: 20
end: completed ("Refund closed.")
handoff: refund_handoff ("Bring in a refund specialist.")
vars: order_id:text, amount:number:optional
---
Ask for @order_id.
# Check eligibility
Call the refund tool #refund.check[in order=@order_id, locale=ctx.page_locale; out status=@refund_status]
[if refund_status = approved] -> end
[outcome failed] -> handoffFrontmatter fields:
grammar— integer grammar version; a missing key parses as1.name,trigger— display name and activation trigger; both default to empty when omitted.vars— comma-separated slot declarations,key:type[:optional][:mutable]. Valid types aretext,number,boolean,email, anddate. A requiredtextvariable referenced in the body as a bare@namedoesn’t need avarsentry.reentry—once_per_conversation(default),always, orsemantic.priority— integer activation priority; defaults to0.export— completion export,<complete|handoff|complete,handoff> -> <destinationRef>, referencing a webhook destination id.end,handoff— the primary completion and handoff terminals, each<id>or<id> ("message").
Inline tokens in the body:
@namereferences a slot variable.#skill_namecalls a skill, optionally with typed bindings:#crm.lookup[in email=@email, locale=ctx.page_locale, tier=gold; out account_id=@account_id]. An input can be a literal (tier=gold), a slot (in order=@order_id), or a context variable (locale=ctx.page_locale); an output must assign a slot without name=@slot.[action "type"]records an action marker.# Titlestarts a named step; following lines belong to it until the next heading.
Branch lines start with a guard and end with -> <target>:
- Field guards:
[if amount >= 100],[if country in US, CA],[if order_date older than 30 days],[if email is present]. Operators are=,!=,in,>,>=,<,<=,is true,is false,is present,is absent,older than, andwithin. - Skill outcome guards:
[outcome failed]. - Slot-filled guards:
[filled @email, @order_id]. - A branch line with no deterministic guard is an LLM guard — its prose becomes the text the runtime asks the model to judge.
- Targets:
-> end,-> end:name ("Message"),-> handoff,-> step:step_id, or-> step:step_id (max 3)for a bounded loop. A loop’s(max N)can’t combine with[if ...],[outcome ...], or[filled ...]on the same line.
Decision and approval gates pause for a choice:
[decision refund_decision: approve="Approve", deny="Deny"]
[approval refund_decision: approve="Approve" -> end, deny="Deny" ("Needs review") -> handoff]Decision options don’t need route targets; approval options do.
Commit-back workflow
For routines stored in a repository, canonicalize before committing.
- Edit the markdown file.
- Call
POST /api/v1/routines/portable/canonicalize. - Replace the file content with the returned
content. - Commit the canonical file.
- Send that envelope to create or update the routine.
Canonicalize only checks grammar and formatting. Create and update still run the routine validator.
Error split
400 means the markdown failed grammar parsing. The response contains line-based
diagnostics, each with line, code, and message — for example
unsupported_grammar_version when grammar isn’t 1, or invalid_target_token
when a -> target doesn’t match the stable-id grammar.
422 means the markdown parsed, but the resulting routine definition failed
routine validation. GET and PUT can also return 422 when a valid structured
routine cannot be represented as portable markdown v1, such as a routine with
multiple handoff terminals or an activation gate (GET returns
routine_not_portable for a gated routine; PUT can still update it, preserving
the existing gate while applying the markdown body).
409 means the create request conflicts with an existing routine definition,
such as the same routine name and version for the agent.
Read next
- Author a routine — the dashboard editor that reads and writes this same format.
- Agents and skills
- API Reference