Everything you need to integrate Unaka into your agent.
Call /plan with a natural language intent and a provider. Get back a structured execution plan with the exact API calls to make. Execute with your own auth. No API knowledge required.
Sign up at unaka.io — your key is shown once, copy it immediately. Pass it as Authorization: Bearer <key> or X-API-Key: <key> on every request.
Send a POST request with your intent and provider. Minimal request:
{
"intent": "create urgent bug for login timeout",
"provider": "linear"
}Full request with context for entity resolution and observability:
{
"intent": "create urgent bug for login timeout assign to Sarah",
"provider": "linear",
"context": {
"plan": {
"teams": [{ "id": "36824a37-...", "name": "Engineering" }],
"members": [{ "id": "0abb0e37-...", "name": "Sarah Chen" }],
"labels": [{ "id": "uuid", "name": "bug" }],
"cycles": [{ "id": "uuid", "name": "Sprint 12", "active": true }]
},
"observe": {
"agent_id": "agent_abc123",
"agent_name": "issue-triage-bot",
"session_id": "session_xyz",
"previous_plan_id": "plan_abc123",
"user_id": "user_456"
}
}
}context.plan — pre-fetched provider data for resolving entity references. Pass typed entity lists (teams, members, labels, cycles) and Unaka maps them to provider-specific field names.
context.observe — traceability metadata for the Observe dashboard. Never affects plan generation.
The response will be one of four statuses:
| ready_to_hydrate | Substitute placeholders in actions[] and execute. |
| missing_information | Resolve the prerequisites[], then re-call /plan with context. |
| not_supported | No matching workflow. similar[] shows nearby supported workflows. |
| parsing_failed | Confidence too low. candidates[] shows closest matches to retry with. |
{
"plan_id": "plan_9a145d4", // track this for callbacks
"status": "ready_to_hydrate", // substitute placeholders, then execute
"workflow": "createIssue", // what Unaka decided to do
"confidence": 0.90,
"actions": [
{
"method": "POST",
"endpoint": "https://api.linear.app/graphql",
"headers": {
"Authorization": "{{LINEAR_API_KEY}}", // from your environment
"Content-Type": "application/json"
},
"body": {
"query": "mutation CreateIssue($input: IssueCreateInput!) { ... }",
"variables": {
"input": {
"teamId": "{{context.plan.team_id}}",// from context.plan
"title": "Login timeout", // resolved from intent
"priority": 1 // "urgent" → 1
}
}
},
"outputs": {
"issue_id": "data.issueCreate.issue.id",
"issue_identifier": "data.issueCreate.issue.identifier"
},
"depends_on": []
}
],
"suggestions": [ // optional enrichment
{
"field": "description",
"hint": "Consider adding a description"
}
]
}When status is ready_to_hydrate, iterate over actions[], substitute placeholders ({{PROVIDER_API_KEY}}, {{context.plan.*}}), resolve step dependencies, and fire each HTTP call.
import os, re, requests
def execute_plan(plan_response, context_plan=None):
"""Execute a ready_to_hydrate plan. Returns (success, outputs)."""
actions = plan_response["actions"]
env = {"LINEAR_API_KEY": os.environ["LINEAR_API_KEY"]}
# Merge context.plan values into env for placeholder substitution
if context_plan:
for key, val in context_plan.items():
env[f"context.plan.{key}"] = str(val)
outputs = {} # {action_index: {name: value}}
for i, action in enumerate(actions):
# Substitute {{placeholders}} in headers and body
headers = _resolve(action["headers"], env, outputs)
body = _resolve(action["body"], env, outputs)
resp = requests.request(
action["method"],
action["endpoint"],
headers=headers,
json=body,
timeout=30,
)
data = resp.json()
# GraphQL returns 200 even on failure — always check errors
if data.get("errors"):
return False, data["errors"][0]["message"]
# Capture outputs for dependency threading between actions
outputs[i] = {}
for name, path in action.get("outputs", {}).items():
outputs[i][name] = _walk(data, path)
return True, outputs
def _resolve(obj, env, outputs):
"""Recursively substitute {{placeholders}}."""
if isinstance(obj, str):
return re.sub(
r"\{\{(.+?)\}\}",
lambda m: _lookup(m.group(1), env, outputs),
obj,
)
if isinstance(obj, dict):
return {k: _resolve(v, env, outputs) for k, v in obj.items()}
if isinstance(obj, list):
return [_resolve(x, env, outputs) for x in obj]
return obj
def _lookup(key, env, outputs):
# Match {{actions[N].outputs.key}}
m = re.match(r"(?:steps|actions)\[(\d+)\]\.outputs\.(\w+)", key)
if m:
return str(outputs.get(int(m.group(1)), {}).get(m.group(2), ""))
return env.get(key, "{{" + key + "}}")
def _walk(data, path):
"""Walk a dot-separated path into nested dicts."""
for k in path.split("."):
if not isinstance(data, dict):
return None
data = data.get(k)
return dataresponse.errors after every action — don't rely on HTTP status codes alone.After execution, fire a callback so the catalog improves from real execution data.
curl -X POST https://api.unaka.io/plan/result \
-H "Authorization: Bearer <your-api-key>" \
-H "Content-Type: application/json" \
-d '{
"plan_id": "plan_9a145d4",
"success": true
}'On failure, include error_code and provider_error so we know what went wrong.
Some intents resolve fully without any context — priority mappings, identifier references, and title extraction work from the intent alone. Others return missing_information when entity references can't be resolved without workspace-specific IDs.
When a plan returns missing_information, resolve the prerequisites and re-call /plan with context.plan:
{
"intent": "create urgent bug for the backend team",
"provider": "linear",
"context": {
"plan": {
"teams": [{ "id": "36824a37-...", "name": "Backend" }]
}
}
}The prerequisites array tells you exactly what's missing, what type of entity it is, and how to resolve it: select (present a picker), lookup (query the provider API), or text (ask the user).