Documentation

Everything you need to integrate Unaka into your agent.

For humansFor agents

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.


Get your API key

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.


Call /plan

Send a POST request with your intent and provider. Minimal request:

json
{
  "intent": "create urgent bug for login timeout",
  "provider": "linear"
}

Full request with context for entity resolution and observability:

json
{
  "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_hydrateSubstitute placeholders in actions[] and execute.
missing_informationResolve the prerequisites[], then re-call /plan with context.
not_supportedNo matching workflow. similar[] shows nearby supported workflows.
parsing_failedConfidence 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"
    }
  ]
}

Execute the plan

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 data
Gotcha — GraphQL errors return HTTP 200
Linear's GraphQL API returns HTTP 200 even when a mutation fails. Always check response.errors after every action — don't rely on HTTP status codes alone.

Report back

After execution, fire a callback so the catalog improves from real execution data.

bash
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.


What to expect

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:

json
{
  "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).