Back to blog
Engineering
June 7, 20268 min read

Controlling Agentis from your own code: a tour of the SDK

A mention in Slack or a comment on a pull request is one way to start an agent. Your own code is another. The Agentis SDK is a Python client, a CLI and an MCP server over one JSON-RPC endpoint — create tasks, read runs, and block on a human answer, straight from a script, a CI job or a cron.

Ondřej Novák

Founder

The Slack and GitHub bridges exist because most agent work starts with a human typing somewhere. But not all of it does. Sometimes the thing that should start an agent is a failing nightly build, a monitoring alert, a row that landed in a queue, or a deploy script that wants a human to sign off before it touches production. None of those have a chat window to mention a bot in — they have code. The Agentis SDK is the surface for exactly that case: a small Python library, a CLI that wraps it, and an MCP server, all talking to the same backend the web app does.

One endpoint, three front doors

Everything in Agentis is a JSON-RPC 2.0 call to a single endpoint — POST /api. The SDK never invents its own protocol; it is a typed wrapper around that endpoint with the polling and error handling already written. You pick the front door that fits where you are calling from.

  • AgentisClient — a typed Python client for creating tasks, reading runs, and asking the user questions or for approvals.
  • agentis — a CLI that exposes every client capability for shell scripts and quick manual calls.
  • agentis-mcp — a stdio MCP server that turns question and approve into agent tools, so a running agent can pause and ask the human.

Connection settings come from the environment — AGENTIS_API_URL, AGENTIS_API_TOKEN and an optional AGENTIS_API_TIMEOUT — and constructor arguments or CLI flags override them. The token is the backend service token, sent on every request. Both the CLI and the MCP server auto-load a local .env from the working directory, so configuration can live next to the project instead of in your shell profile.

Creating a task in three lines

The client reads its URL and token from the environment, so the smallest useful program is short. A plain-text description is wrapped into the backend’s Lexical document format for you; create_task returns the freshly fetched task, and the id you will reference everywhere lives at task["form"]["id"].

from agentis_sdk import AgentisClient

client = AgentisClient()  # base_url + token from the environment

task = client.create_task(
    title="Investigate the nightly build failure",
    description="The 02:00 job failed on test_checkout.\nReproduce, then patch.",
)
task_id = task["form"]["id"]

That is the floor, not the ceiling. create_task takes the same knobs the web form exposes — project, agent, model, effort, adapter and environment, plus status, priority, labels and a worktree flag — all optional keyword arguments. Anything the typed signature does not cover can be passed through an extra mapping, so the client never blocks you from a field the backend understands.

Reading back what happened

A task id is a handle to a whole history. get_task returns the task and everything hanging off it — its form, its runs, comments, actions, todos and questions. To look inside a single run (an AI session), get_run returns the full run metadata, the run items and parts, token usage and comments; get_session is an alias for it, because a run is just the persisted record of a session. And when your own automation does work worth showing on the run’s timeline, add_adapter_message appends an event — a kind, a started/success/failed status, an optional message and structured data — with an optional event id for idempotency.

Asking the human — and waiting for the answer

The part that turns a script into a governed workflow is the reverse direction: your code asking a person and blocking on the reply. add_question posts one or more questions for the user. By default it is fire-and-forget — it returns the moment the question is stored, handing you back an external_id you can poll later. Pass wait=True and it blocks instead, polling task.get_question_result every poll_interval seconds until the batch is answered, and raising QuestionTimeout if the deadline passes first.

answer = client.add_question(
    {
        "question": "Which environment should I deploy to?",
        "options": [{"label": "staging"}, {"label": "production"}],
        "multiple": False,
    },
    wait=True,
    timeout=600,
)
print(answer["answers"])  # e.g. [["staging"]]

A question is flexible: pass a single string, a single dict, or a list of either. A dict can carry a header, predefined options, a multiple flag and a free-text escape hatch. The waiting is not special-cased into add_question either — wait_for_answer is a public method you can call yourself against a stored external_id, which is exactly how a fire-and-forget question gets resolved later by a different process.

Approvals are a one-bit decision

An approval is the narrow case of a question: not free-form, just yes or no, optionally with a comment. add_approve mirrors add_question — fire-and-forget by default, blocking with wait=True against task.get_approve_result, ApproveTimeout on the deadline. The decided payload exposes a boolean approved and an optional comment, so a deploy gate reads exactly like the intent behind it.

decision = client.add_approve(
    "Deploy build #1234 to production?",
    title="Production deploy",
    wait=True,
    timeout=600,
)
if decision["approved"]:
    deploy(comment=decision.get("comment"))
else:
    abort(decision.get("comment"))

Fire-and-forget when you trust the agent to proceed; block on a human when you don’t. It is the same call with one flag flipped.

The CLI is the same client, minus the import

Everything the library does, the agentis command does too — it is a thin shell over AgentisClient, so the behavior, including the polling and the timeouts, is identical. It prints the JSON result to stdout and sends errors to stderr with a non-zero exit code, which makes it drop straight into a Makefile, a CI step or a one-off terminal call. The global --base-url, --token and --timeout flags work on every subcommand.

agentis task create --title "Fix bug" --description "Reproduce then patch"
agentis task get <task-uuid>
agentis run get <run-uuid>

agentis approve add \
  --description "Deploy build #1234 to production?" \
  --title "Production deploy" \
  --wait --wait-timeout 600

Handing the question back to a running agent

The third front door inverts the relationship. agentis-mcp is a stdio MCP server that exposes question and approve as tools, so an agent — Claude Code, for instance — can stop mid-run and hand control back to a human. It is built on the same AgentisClient, so the JSON-RPC and polling logic is shared rather than reimplemented; the agent calls a tool, the human answers in the Agentis UI, and the tool returns the answer. Registering it is one block of MCP config pointing at the command.

{
  "mcpServers": {
    "agentis": {
      "command": "agentis-mcp",
      "env": {
        "AGENTIS_API_URL": "https://agentis.cz/api",
        "AGENTIS_API_TOKEN": "my-service-token"
      }
    }
  }
}

The server resolves which session is asking from the Claude Code harness — a /tmp/claude-session-<pid> file — falling back to AGENTIS_DEFAULT_SESSION_ID, and it has its own knobs for the answer timeout, the poll interval and a rotating log file. The point is that the human-in-the-loop primitive is the same one whether your code calls add_question directly or an agent reaches it through MCP.

When things go wrong

Automation that talks to a network has to fail legibly. Every exception in the SDK derives from one base, AgentisError, so a script can catch broadly or precisely. The split is along the line that matters: did the transport fail, or did the backend say no?

  • AgentisTransportError — the HTTP layer failed: a connection problem, a timeout, an empty or non-JSON body.
  • AgentisRPCError — the backend returned a JSON-RPC error, with .code and .data carried through so you can branch on it.
  • QuestionTimeout — a synchronous question wait outlived its timeout.
  • ApproveTimeout — a synchronous approval wait outlived its timeout.

That is the whole surface. The SDK is not a second system — it is the same control plane the Slack and GitHub bridges sit on, exposed to your own code. A cron job, a CI step, a webhook handler or a deploy script can now open a tracked task, watch its run, and pause for a human decision, with the same history, cost accounting and audit trail as everything else in Agentis. The bridges turn a mention into a task; the SDK turns a line of your code into one.

See Agentis in action on your own task

Sign in, describe a task and watch an agent deliver reviewed, ready-to-ship work in minutes.

Try Agentis now

Keep reading