Axernel SDK

Official TypeScript and Python clients for the Axernel API. Configure agents, open sessions, create runs, stream events, and read results and artifacts.

Both clients are generated from the API's OpenAPI contract, with handwritten pagination, run waiting, server-sent events, uploads, downloads, typed errors, retries and idempotency on top.

Install

Not published yet. The packages are not on npm or PyPI today. They ship with the technical preview. Join the waitlist for access.

Shell
npm install @axernel/sdk
Shell
pip install axernel

Requires Node.js 20 or later. Ships as ES modules with TypeScript types.Requires Python 3.10 or later. Includes a synchronous and an asynchronous client, with Pydantic models generated from the OpenAPI contract.

Configure the client

Set the API endpoint and a platform API token. The client reads both from the environment.

Shell
export AXERNEL_BASE_URL=http://localhost:8080
export AXERNEL_API_KEY=axk_...
TypeScript
import { Axernel } from "@axernel/sdk"

const axernel = new Axernel()
Python
from axernel import Axernel, models

axernel = Axernel()

Constructor options override the environment.Constructor arguments override the environment.

TypeScript
const axernel = new Axernel({
  baseUrl: "https://axernel.example.com",
  apiKey: process.env.MY_AXERNEL_TOKEN,
  timeoutMs: 60_000,
  maxRetries: 2,
})
Python
axernel = Axernel(
    base_url="https://axernel.example.com",
    api_key="axk_...",
    timeout=60,
    max_retries=2,
)

The client is a context manager, and AsyncAxernel exposes the same resource names.

Python
from axernel import AsyncAxernel

async with AsyncAxernel() as axernel:
    run = await axernel.runs.get("run_id")
    async for event in axernel.runs.events(run.id):
        print(event.type.value)

Quickstart

Give an agent a task that needs a real tool, and get back a typed answer. The agent below runs a shell command in its sandbox to hash a string. The whole flow is six calls.

1. Create a project and pick the platform defaults

A project owns everything else. The model provider and the sandbox template come from the platform catalog, so there is nothing to configure.

TypeScript
import { Axernel } from "@axernel/sdk"

const axernel = new Axernel()

const project = await axernel.projects.create({ name: "SDK quickstart" })
const provider = await axernel.modelProviders.getPlatform("openrouter-deepseek")
const template = await axernel.templates.getPlatform("general-purpose")
Python
from axernel import Axernel, models

axernel = Axernel()

project = axernel.projects.create(models.CreateProjectRequest(name="SDK quickstart"))
provider = axernel.model_providers.get_platform("openrouter-deepseek")
template = axernel.templates.get_platform("general-purpose")

2. Define the agent

An agent is instructions plus two JSON Schema contracts: what a task must look like going in, and what the answer must look like coming out.

TypeScript
const agent = await axernel.agents.create(project.id, {
  name: "Hasher",
  templateId: template.id,
  configuration: {
    harness: "opencode",
    modelProviderId: provider.id,
    instructions: "Use the shell to do what the task asks. Submit only the requested result.",
    contracts: {
      input: {
        schema: {
          type: "object",
          properties: { task: { type: "string" } },
          required: ["task"],
          additionalProperties: false,
        },
      },
      output: {
        schema: {
          type: "object",
          properties: { digest: { type: "string" } },
          required: ["digest"],
          additionalProperties: false,
        },
      },
    },
    limits: { timeoutSeconds: 900, maxSteps: 100 },
    artifacts: [],
  },
})
Python
agent = axernel.agents.create(
    project.id,
    models.CreateAgentRequest(
        name="Hasher",
        template_id=template.id,
        configuration=models.AgentConfiguration(
            harness=models.Harness.opencode,
            model_provider_id=provider.id,
            instructions="Use the shell to do what the task asks. Submit only the requested result.",
            contracts=models.Contracts(
                input=models.Contract(
                    schema_={
                        "type": "object",
                        "properties": {"task": {"type": "string"}},
                        "required": ["task"],
                        "additionalProperties": False,
                    }
                ),
                output=models.Contract(
                    schema_={
                        "type": "object",
                        "properties": {"digest": {"type": "string"}},
                        "required": ["digest"],
                        "additionalProperties": False,
                    }
                ),
            ),
            limits=models.AgentLimits(timeout_seconds=900, max_steps=100),
            artifacts=[],
        ),
    ),
)

3. Open a session and send the task

A session pins this version of the agent. A run is one task inside it.

TypeScript
const session = await axernel.sessions.create(project.id, {
  agentId: agent.id,
  checkpointingEnabled: true,
})

const run = await axernel.runs.create(session.id, {
  input: {
    data: { task: "Compute the SHA-256 of the text 'hello axernel' with a shell command." },
  },
})
Python
session = axernel.sessions.create(
    project.id,
    models.CreateSessionRequest(agent_id=agent.id, checkpointing_enabled=True),
)

run = axernel.runs.create(
    session.id,
    models.CreateRunRequest(
        input=models.RunInput(
            data=models.JSONValue(
                {"task": "Compute the SHA-256 of the text 'hello axernel' with a shell command."}
            )
        )
    ),
)

4. Read the result

runs.result() waits for the run to finish and returns the value the agent submitted, already validated against the output schema.runs.result() waits for the run to finish and returns the value the agent submitted, already validated against the output schema.

TypeScript
const result = await axernel.runs.result<{ digest: string }>(run.id)

console.log(result.digest)
Python
result = axernel.runs.result(run.id)

print(result["digest"])
Output
306d5032cf03e9480b0a17523bdd7f69c305e7102471c251e7d265e8d9d26947

Axernel prepared a sandbox, started the harness with your instructions, let the model call the shell, checked the answer against the contract, and returned it. To watch that happen, stream run events while the run is in flight. To keep going, send another run to the same session: it reuses the sandbox and the conversation.

Resource model

An application normally creates resources in this order.

Project
  ├── Secrets and uploaded files
  └── Agent
        ├── Template
        ├── Model provider
        ├── Instructions
        ├── Input and output schemas
        ├── MCP and environment credential bindings
        └── Execution limits and artifact requirements

Agent → Session → Run → Events, result, usage and artifacts
  • A project owns agents, secrets, files, and sessions.
  • A template defines the sandbox image, available environment tools, preparation budget, maximum sandbox lifetime, and idle retention.
  • A model provider defines the API protocol, endpoint, model, pricing, and private token.
  • An agent is a reusable, revisioned configuration.
  • A session pins one agent revision. Later agent edits do not alter existing sessions.
  • A run supplies task-specific input to that pinned contract.

Projects

TypeScript
const project = await axernel.projects.create({
  name: "Pull request automation",
  description: "Agents that inspect and update repositories",
})
Python
project = axernel.projects.create(
    models.CreateProjectRequest(
        name="Pull request automation",
        description="Agents that update repositories",
    )
)

Projects can also be listed, iterated, read, and updated.

TypeScript
const page = await axernel.projects.list({ page: 1, pageSize: 50 })

for await (const project of axernel.projects.iterate()) {
  console.log(project.id, project.name)
}
Python
page = axernel.projects.list(page=1, page_size=50)

for project in axernel.projects.iterate():
    print(project.id, project.name)

Model providers and templates

Resolve platform-managed resources by their stable installation keys. These endpoints return active platform-owned resources only. Every returned resource has managedBy: "platform" and editable: false.Resolve platform-managed resources by their stable installation keys. These endpoints return active platform-owned resources only. Every returned resource has managed_by=platform and editable=False.

TypeScript
const provider = await axernel.modelProviders.getPlatform("openrouter-deepseek")
const template = await axernel.templates.getPlatform("general-purpose")
Python
provider = axernel.model_providers.get_platform("openrouter-deepseek")
template = axernel.templates.get_platform("general-purpose")

Use listPlatform() or iteratePlatform() to discover the keys configured by an installation. list() and iterate() return the combined catalog of platform resources and resources owned by the authenticated organization.Use list_platform() or iterate_platform() to discover the keys configured by an installation. list() and iterate() return the combined catalog of platform resources and resources owned by the authenticated organization.

Bring your own provider

An organization can register its own OpenAI-compatible provider. The token is accepted on creation or update and is never returned.

TypeScript
const provider = await axernel.modelProviders.create({
  name: "Company inference gateway",
  protocol: "openai_compatible",
  baseUrl: "https://models.example.com/v1",
  modelName: "company/agent-model",
  token: process.env.MODEL_PROVIDER_TOKEN!,
  pricing: {
    inputUsdPerMillion: 0.25,
    outputUsdPerMillion: 1.0,
    reasoningUsdPerMillion: 1.0,
    cacheReadUsdPerMillion: 0.025,
    cacheWriteUsdPerMillion: 0.25,
  },
})
Python
provider = axernel.model_providers.create(
    models.CreateModelProviderRequest(
        name="Company inference gateway",
        protocol=models.ModelProviderProtocol.openai_compatible,
        base_url="https://models.example.com/v1",
        model_name="company/agent-model",
        token=os.environ["MODEL_PROVIDER_TOKEN"],
        pricing=models.ModelPricing(
            input_usd_per_million=0.25,
            output_usd_per_million=1.0,
            reasoning_usd_per_million=1.0,
            cache_read_usd_per_million=0.025,
            cache_write_usd_per_million=0.25,
        ),
    )
)

Custom sandbox image

Create a template when the agent needs its own image.

TypeScript
const template = await axernel.templates.create({
  name: "Coding environment",
  image_ref: "registry.example.com/agents/coding@sha256:<digest>",
  environmentTools: [
    { name: "git", description: "Git command-line client" },
    { name: "gh", description: "GitHub command-line client" },
    { name: "node", description: "Node.js runtime" },
  ],
  preparationTimeoutSeconds: 600,
  sandboxMaxLifetimeSeconds: 86_400,
  sandboxIdleTimeoutSeconds: 1_800,
})
Python
template = axernel.templates.create(
    models.CreateTemplateRequest(
        name="Coding environment",
        image_ref="registry.example.com/agents/coding@sha256:<digest>",
        environment_tools=[
            models.EnvironmentTool(name="git", description="Git command-line client"),
            models.EnvironmentTool(name="gh", description="GitHub command-line client"),
            models.EnvironmentTool(name="node", description="Node.js runtime"),
        ],
        preparation_timeout_seconds=600,
        sandbox_max_lifetime_seconds=86_400,
        sandbox_idle_timeout_seconds=1_800,
    )
)

Secrets

Secrets belong to a project. Values are accepted by create and update requests but are never returned by get or list operations. Responses expose only metadata and key names.

TypeScript
const githubSecret = await axernel.secrets.create(project.id, {
  name: "GitHub service account",
  description: "Repository access for coding agents",
  data: {
    token: process.env.GITHUB_TOKEN!,
  },
})

console.log(githubSecret.keys) // ["token"]
Python
import os

github_secret = axernel.secrets.create(
    project.id,
    models.CreateSecretRequest(
        name="GitHub service account",
        description="Repository access for coding agents",
        data=models.SecretData({"token": os.environ["GITHUB_TOKEN"]}),
    ),
)

Rotate a value with an update. Delete removes a secret that is no longer referenced.

TypeScript
await axernel.secrets.update(project.id, githubSecret.id, {
  data: { token: process.env.ROTATED_GITHUB_TOKEN! },
})

await axernel.secrets.delete(project.id, githubSecret.id)
Python
axernel.secrets.update(
    project.id,
    github_secret.id,
    models.UpdateSecretRequest(
        data=models.SecretData({"token": os.environ["ROTATED_GITHUB_TOKEN"]}),
    ),
)

axernel.secrets.delete(project.id, github_secret.id)

Do not place secret values in agent instructions, input data, metadata, or MCP URLs. Agent configuration stores a secret ID and key. The runtime resolves the value when it admits a run.

Contracts

Contracts are JSON Schema objects. input.data must match the input schema before the run is queued. The agent's submitted response.value must match the output schema before the run can complete.Contracts are JSON Schema objects. run.input.data must match the input schema before the run is queued. The agent's submitted value must match the output schema before the run can complete.

TypeScript
const inputSchema = {
  type: "object",
  properties: {
    repository: { type: "string" },
    issueNumber: { type: "integer", minimum: 1 },
    task: { type: "string" },
  },
  required: ["repository", "issueNumber", "task"],
  additionalProperties: false,
}

const outputSchema = {
  type: "object",
  properties: {
    pullRequestUrl: { type: "string" },
    summary: { type: "string" },
  },
  required: ["pullRequestUrl", "summary"],
  additionalProperties: false,
}
Python
input_contract = models.Contract(
    schema_={
        "type": "object",
        "properties": {
            "repository": {"type": "string"},
            "issueNumber": {"type": "integer", "minimum": 1},
            "task": {"type": "string"},
        },
        "required": ["repository", "issueNumber", "task"],
        "additionalProperties": False,
    }
)
output_contract = models.Contract(
    schema_={
        "type": "object",
        "properties": {
            "pullRequestUrl": {"type": "string"},
            "summary": {"type": "string"},
        },
        "required": ["pullRequestUrl", "summary"],
        "additionalProperties": False,
    }
)

The schema profile and request limits supported by a deployment are available from the capabilities endpoint.

TypeScript
const capabilities = await axernel.system.capabilities()
console.log(capabilities.schemaProfile)
Python
capabilities = axernel.system.capabilities()
print(capabilities.schema_profile)

Agents

This agent receives GitHub access as GH_TOKEN from the project secret, and uses the same secret to authenticate a remote MCP server.

TypeScript
const agent = await axernel.agents.create(project.id, {
  name: "Issue fixer",
  templateId: template.id,
  configuration: {
    harness: "opencode",
    modelProviderId: provider.id,
    instructions: [
      "Read the requested issue and repository carefully.",
      "Make the smallest correct change and run relevant validation.",
      "Open a pull request and submit its URL with a concise summary.",
    ].join("\n"),
    contracts: {
      input: { schema: inputSchema },
      output: { schema: outputSchema },
    },
    limits: {
      timeoutSeconds: 1_800,
      maxSteps: 200,
    },
    environmentCredentialBindings: {
      GH_TOKEN: {
        secret: { secretId: githubSecret.id, key: "token" },
      },
    },
    mcpServers: {
      github: {
        remote: {
          url: "https://mcp.example.com/github",
          authentication: {
            type: "bearer",
            credential: {
              secret: { secretId: githubSecret.id, key: "token" },
            },
          },
        },
      },
    },
    artifacts: [],
  },
})
Python
github_credential = models.CredentialSource(
    secret=models.SecretCredentialSource(
        secret_id=github_secret.id,
        key="token",
    )
)

agent = axernel.agents.create(
    project.id,
    models.CreateAgentRequest(
        name="Issue fixer",
        template_id=template.id,
        configuration=models.AgentConfiguration(
            harness=models.Harness.opencode,
            model_provider_id=provider.id,
            instructions=(
                "Read the requested issue, make the smallest correct change, "
                "run relevant validation, open a pull request, and submit its URL."
            ),
            contracts=models.Contracts(
                input=input_contract,
                output=output_contract,
            ),
            limits=models.AgentLimits(
                timeout_seconds=1800,
                max_steps=200,
            ),
            environment_credential_bindings={
                "GH_TOKEN": github_credential,
            },
            mcp_servers={
                "github": models.MCPServer(
                    remote=models.MCPRemote(
                        url="https://mcp.example.com/github",
                        authentication=models.MCPAuthentication(
                            type=models.MCPAuthenticationType.bearer,
                            credential=github_credential,
                        ),
                    )
                )
            },
            artifacts=[],
        ),
    ),
)

Configuration fields

FieldPurpose
harnessHarness implementation used to execute turns. V1 supports opencode.
modelProviderIdOrganization or platform model provider used by the harness.
instructionsReusable behavior and operating instructions for every run.
contracts.input.schemaValidates run.input.data before admission.
contracts.output.schemaValidates the value submitted by the agent.
limitsDefault execution timeout and maximum model steps.
mcpServersRemote and stdio MCP servers configured before the prompt begins.
environmentCredentialBindingsSecrets or per-run values injected into harness and tool environment variables.
artifactsFiles collected after the agent submits an accepted result.
FieldPurpose
harnessHarness implementation used to execute turns. V1 supports opencode.
model_provider_idOrganization or platform model provider used by the harness.
instructionsReusable behavior and operating instructions for every run.
contracts.inputValidates run.input.data before admission.
contracts.outputValidates the value submitted by the agent.
limitsDefault execution timeout and maximum model steps.
mcp_serversRemote and stdio MCP servers configured before the prompt begins.
environment_credential_bindingsSecrets or per-run values injected into harness and tool environment variables.
artifactsFiles collected after the agent submits an accepted result.

Revisions

Agent updates create a new revision. The expected revision prevents overwriting a concurrent update.

TypeScript
const updated = await axernel.agents.update(
  project.id,
  agent.id,
  {
    configuration: {
      ...agent.configuration,
      instructions: `${agent.configuration.instructions}\nPrefer focused tests.`,
    },
  },
  { expectedRevision: agent.revision },
)
Python
updated = axernel.agents.update(
    project.id,
    agent.id,
    models.UpdateAgentRequest(configuration=agent.configuration),
    expected_revision=agent.revision,
)

MCP servers

MCP servers are part of the agent revision. A remote server declares standard authentication and additional request headers separately. Stdio environment bindings receive the credential value unchanged.

Credential from a project secret

Use a project secret for a reusable credential.

TypeScript
const mcpServers = {
  github: {
    remote: {
      url: "https://mcp.example.com/github",
      authentication: {
        type: "bearer",
        credential: {
          secret: { secretId: githubSecret.id, key: "token" },
        },
      },
    },
  },
}
Python
github_mcp = models.MCPServer(
    remote=models.MCPRemote(
        url="https://mcp.example.com/github",
        authentication=models.MCPAuthentication(
            type=models.MCPAuthenticationType.bearer,
            credential=models.CredentialSource(
                secret=models.SecretCredentialSource(
                    secret_id=github_secret.id,
                    key="token",
                ),
            ),
        ),
    )
)

Credential supplied with every run

Require a fresh credential with every run when the calling application owns the credential lifecycle.

TypeScript
const mcpServers = {
  github: {
    remote: {
      url: "https://mcp.example.com/github",
      authentication: {
        type: "bearer",
        credential: { runRequestCredential: "githubToken" },
      },
    },
  },
}
Python
github_mcp = models.MCPServer(
    remote=models.MCPRemote(
        url="https://mcp.example.com/github",
        authentication=models.MCPAuthentication(
            type=models.MCPAuthenticationType.bearer,
            credential=models.CredentialSource(
                run_request_credential="githubToken",
            ),
        ),
    )
)

Exactly one of secret or runRequestCredential is allowed per credential source. Bearer authentication always sends Authorization: Bearer <value>. API-key authentication additionally requires headerName. Use requestHeaders for dynamic custom headers.Exactly one of secret or run_request_credential is allowed per credential source. Bearer authentication always sends Authorization: Bearer <value>. API-key authentication additionally requires header_name. Use request_headers for dynamic custom headers.

TypeScript
const remote = {
  url: "https://mcp.example.com/tools",
  authentication: {
    type: "api_key" as const,
    headerName: "X-API-Key",
    credential: { runRequestCredential: "toolsApiKey" },
  },
  requestHeaders: [
    {
      name: "X-Workspace-ID",
      value: { secret: { secretId: workspaceSecret.id, key: "id" } },
    },
  ],
}
Python
remote = models.MCPRemote(
    url="https://mcp.example.com/tools",
    authentication=models.MCPAuthentication(
        type=models.MCPAuthenticationType.api_key,
        header_name="X-API-Key",
        credential=models.CredentialSource(run_request_credential="toolsApiKey"),
    ),
    request_headers=[
        models.MCPRequestHeader(
            name="X-Workspace-ID",
            value=models.CredentialSource(
                secret=models.SecretCredentialSource(
                    secret_id=workspace_secret.id,
                    key="id",
                ),
            ),
        )
    ],
)

Stdio servers

A stdio MCP server must already be installed in the template image.

TypeScript
const mcpServers = {
  tools: {
    stdio: {
      command: [
        "env",
        "TOOLS_DEPLOYMENT_URL=https://tools.example.com",
        "node",
        "/opt/tools-mcp/index.js",
      ],
      environmentBindings: {
        TOOLS_TOKEN: { runRequestCredential: "toolsToken" },
      },
    },
  },
}
Python
tools_mcp = models.MCPServer(
    stdio=models.MCPStdio(
        command=[
            "env",
            "TOOLS_DEPLOYMENT_URL=https://tools.example.com",
            "node",
            "/opt/tools-mcp/index.js",
        ],
        environment_bindings={
            "TOOLS_TOKEN": models.CredentialSource(
                run_request_credential="toolsToken"
            )
        },
    )
)

Lifecycle for each run

Run admission
  → validate every required binding
  → resolve project-secret values and request-supplied values
  → send values separately from the persisted assignment
  → supervisor configures remote and stdio MCP servers in the harness
  → require every configured MCP server to connect before prompting
  → execute the turn

Credential values are not written into agent configuration, instructions, run input, events, or checkpoints.

Rules

  • Each server selects exactly one transport: remote or stdio.
  • Remote URLs cannot contain embedded credentials. Native MCP OAuth is disabled because runs are asynchronous. Supply an explicit credential binding instead.
  • Stdio commands must already exist in the template image. The platform does not install an MCP package during a run.
  • Environment bindings are for secret values. Put fixed, non-secret configuration in the image, a wrapper executable, or an env command argument as shown above.
  • Server names may contain letters, digits, underscores, and hyphens. result is reserved for the platform result-submission MCP.
  • Any configured MCP server that cannot connect fails preparation before the model receives the run prompt.

Files

Uploaded files are immutable, belong to a project, and can be attached to a session or to one run.

TypeScript
import { readFile } from "node:fs/promises"

const specification = await axernel.files.upload(project.id, {
  data: await readFile("./openapi.yaml"),
  name: "openapi.yaml",
  mediaType: "application/yaml",
})

const reproduction = await axernel.files.upload(project.id, {
  data: await readFile("./issue-285-reproduction.txt"),
  name: "issue-285-reproduction.txt",
  mediaType: "text/plain",
})
Python
specification = axernel.files.upload(
    project.id,
    "openapi.yaml",
    media_type="application/yaml",
)

reproduction = axernel.files.upload(
    project.id,
    "issue-285-reproduction.txt",
    media_type="text/plain",
)

upload() accepts a path, bytes, or a binary stream.

Sessions

Session creation pins the current agent revision, template settings, model-provider settings, contracts, MCP configuration, limits, and artifact requirements. It does not create a run or a sandbox.

TypeScript
const session = await axernel.sessions.create(
  project.id,
  {
    agentId: agent.id,
    checkpointingEnabled: true,
    attachments: [
      {
        fileId: specification.id,
        description: "API specification to use throughout the conversation",
      },
    ],
    metadata: { externalConversationId: "conversation_42" },
  },
  { idempotencyKey: "create-conversation-42" },
)
Python
session = axernel.sessions.create(
    project.id,
    models.CreateSessionRequest(
        agent_id=agent.id,
        checkpointing_enabled=True,
        attachments=[
            models.Attachment(
                file_id=specification.id,
                description="API specification for this conversation",
            )
        ],
    ),
    idempotency_key="create-conversation-42",
)

Use the same session for sequential runs that should share the retained sandbox and the harness conversation. Close the session when it will receive no more runs.

TypeScript
await axernel.sessions.close(project.id, session.id)
Python
axernel.sessions.close(project.id, session.id)

Runs

Task-specific content belongs in input.data. Run attachments are available only to that run.Task-specific content belongs in input.data. Run attachments are available only to that run.

TypeScript
const run = await axernel.runs.create(
  session.id,
  {
    input: {
      data: {
        repository: "https://github.com/acme/backend",
        issueNumber: 285,
        task: "Fix the issue and open a pull request.",
      },
      attachments: [
        {
          fileId: reproduction.id,
          description: "Issue reproduction details needed only for this run",
        },
      ],
    },
    timeoutSeconds: 1_500,
    maxSteps: 150,
    metadata: { jobId: "job_285" },
  },
  { idempotencyKey: "job-285-attempt-1" },
)
Python
request = models.CreateRunRequest(
    input=models.RunInput(
        data=models.JSONValue(
            {
                "repository": "https://github.com/acme/backend",
                "issueNumber": 285,
                "task": "Fix the issue and open a pull request.",
            }
        ),
        attachments=[
            models.Attachment(
                file_id=reproduction.id,
                description="Issue reproduction details needed only for this run",
            )
        ],
    ),
    timeout_seconds=1500,
    max_steps=150,
)

run = axernel.runs.create(
    session.id,
    request,
    idempotency_key="job-285-attempt-1",
)

A run moves through queued, preparing, running and finalizing, and ends as completed, failed or timed_out.

Run credentials

When the agent requires runRequestCredential bindings, provide their values through the credentials option.When the agent requires run_request_credential bindings, provide their values through the credentials argument.

TypeScript
const run = await axernel.runs.create(session.id, request, {
  idempotencyKey: "job-285-attempt-1",
  credentials: {
    githubToken: process.env.GITHUB_TOKEN!,
    toolsToken: process.env.TOOLS_TOKEN!,
  },
})
Python
run = axernel.runs.create(
    session.id,
    request,
    idempotency_key="job-285-attempt-1",
    credentials={
        "githubToken": os.environ["GITHUB_TOKEN"],
        "toolsToken": os.environ["TOOLS_TOKEN"],
    },
)

The SDK serializes these values into the X-Run-Credentials header. They are separate from the run body and are not exposed as run input or metadata. Missing credentials required by the pinned agent are rejected before the run is queued.

Events and results

Stream progress over server-sent events.

TypeScript
for await (const event of axernel.runs.events(run.id)) {
  switch (event.type) {
    case "run.status":
    case "message.delta":
    case "tool.activity":
    case "harness.status":
      console.log(event.type, event.data)
      break
    case "stream.error":
      console.error(event.data)
      break
  }
}
Python
for event in axernel.runs.events(run.id):
    if event.type.value == "stream.error":
        print("stream error", event.data)
    else:
        # run.status, message.delta, tool.activity, harness.status
        print(event.type.value, event.data)

Resume after the last persisted event sequence.

TypeScript
for await (const event of axernel.runs.events(run.id, { lastEventId: 42 })) {
  // Persist event.sequence after processing each harness event.
}
Python
for event in axernel.runs.events(run.id, last_event_id=42):
    # Persist event.sequence after processing each harness event.
    pass

stream.end closes the event subscription. It does not establish run success. Read the authoritative run afterward.

TypeScript
const terminalRun = await axernel.runs.wait(run.id, {
  timeoutMs: 30 * 60 * 1_000,
  pollIntervalMs: 1_000,
})

if (terminalRun.status === "completed") {
  console.log(terminalRun.response?.value)
} else {
  console.error(terminalRun.error)
}
Python
terminal_run = axernel.runs.wait(run.id, timeout=30 * 60, poll_interval=1)

if terminal_run.status == models.RunStatus.completed:
    print(terminal_run.response.value)
else:
    print(terminal_run.error)

When only the accepted structured value matters, ask for the result.

TypeScript
const result = await axernel.runs.result<{
  pullRequestUrl: string
  summary: string
}>(run.id)

console.log(result.pullRequestUrl)
Python
result = axernel.runs.result(run.id)

print(result["pullRequestUrl"])

runs.result<T>() throws RunFailedError for failed or timed-out runs. The complete terminal run remains available as error.run.runs.result() raises RunFailedError for failed or timed-out runs. The complete terminal run remains available as error.run.

Replies

An agent can complete a turn by asking for required caller input. runs.result<T>() raises RunNeedsInputError. Its run.response.questions contains the stable question IDs and prompts. Answer every question in one linked reply run.An agent can complete a turn by asking for required caller input. runs.result() raises RunNeedsInputError. Its run.response.questions contains the stable question IDs and prompts. Answer every question in one linked reply run.

TypeScript
import { RunNeedsInputError } from "@axernel/sdk"

try {
  const result = await axernel.runs.result(run.id)
  console.log(result)
} catch (error) {
  if (!(error instanceof RunNeedsInputError)) throw error

  const reply = await axernel.runs.reply(
    error.run.sessionId,
    error.run.id,
    { repository_url: "https://github.com/acme/backend" },
    { timeoutSeconds: 600, maxSteps: 100 },
  )
  console.log(await axernel.runs.result(reply.id))
}
Python
from axernel import RunNeedsInputError

try:
    result = axernel.runs.result(run.id)
except RunNeedsInputError as error:
    reply = axernel.runs.reply(
        error.run.session_id,
        error.run.id,
        {"repository_url": "https://github.com/acme/backend"},
        timeout_seconds=600,
        max_steps=100,
    )
    result = axernel.runs.result(reply.id)

The reply continues the same harness conversation. A failed or timed-out reply leaves the original questions pending, so the caller can submit another linked reply.

Before sending, runs.reply() reads the source run, rejects missing or unknown answer keys, and orders answers to match the agent's questions. The API repeats this validation under the session lock.

Artifacts

Artifact requirements belong to the agent configuration.

TypeScript
const artifacts = [
  {
    name: "report",
    fileName: "report.pdf",
    mediaType: "application/pdf",
    required: true,
  },
]
Python
artifacts = [
    models.ArtifactRequirement(
        name="report",
        file_name="report.pdf",
        media_type="application/pdf",
        required=True,
    )
]

Assign this array to configuration.artifacts. After a completed run, inspect run.artifactOutputs.Assign this list to configuration.artifacts. After a completed run, inspect run.artifact_outputs.

TypeScript
const completed = await axernel.runs.wait(run.id)

for (const output of completed.artifactOutputs) {
  if (output.status === "available" && output.artifactId) {
    const metadata = await axernel.artifacts.get(output.artifactId)
    const fileName = metadata.path.split("/").at(-1) ?? metadata.name
    await axernel.artifacts.downloadTo(output.artifactId, `./${fileName}`)
  }
}
Python
completed = axernel.runs.wait(run.id)

for output in completed.artifact_outputs:
    if output.status == models.ArtifactOutputStatus.available and output.artifact_id:
        metadata = axernel.artifacts.get(output.artifact_id)
        file_name = metadata.path.split("/")[-1] or metadata.name
        axernel.artifacts.download_to(output.artifact_id, f"./{file_name}")

The platform collects configured artifacts only after successful result submission. Artifact publication status is separate from the structured result and from checkpoint status.

Authentication

Server applications use a platform API token, as shown in the client configuration. Two other flows exist for applications that sign users in.

Browser applications start GitHub OAuth by navigating to the URL the SDK builds. The Axernel server handles the callback, sets browser cookies, and redirects to the configured UI.

TypeScript
window.location.assign(axernel.auth.githubOAuthUrl())

Password-based applications can obtain tokens through the API and create an authenticated client.

TypeScript
const unauthenticated = new Axernel({ baseUrl: "https://axernel.example.com" })
const auth = await unauthenticated.auth.login({
  email: "[email protected]",
  password: process.env.AXERNEL_PASSWORD!,
})

const authenticated = new Axernel({
  baseUrl: "https://axernel.example.com",
  apiKey: auth.token,
})
Python
unauthenticated = Axernel(base_url="https://axernel.example.com")
auth = unauthenticated.auth.login(
    models.LoginRequest(
        email="[email protected]",
        password=os.environ["AXERNEL_PASSWORD"],
    )
)

authenticated = Axernel(
    base_url="https://axernel.example.com",
    api_key=auth.token,
)

The SDK also exposes signup, refresh, logout, and currentUser.The SDK also exposes signup, refresh, logout, current_user, and github_oauth_url.

Pagination

List methods return the API's page envelope. iterate methods fetch subsequent pages lazily.List methods return the API's page envelope. iterate methods fetch subsequent pages lazily.

TypeScript
const firstPage = await axernel.agents.list(project.id, {
  page: 1,
  pageSize: 50,
})

for await (const agent of axernel.agents.iterate(project.id)) {
  console.log(agent.id)
}
Python
page = axernel.agents.list(project.id, page=1, page_size=50)

for agent in axernel.agents.iterate(project.id):
    print(agent.id)

Pagination is available for model providers, projects, secrets, files, templates, agents, sessions, and runs.

Idempotency and retries

Reads, and requests protected by an idempotency key, use bounded retries for network failures, 408, 429, and common 5xx responses.

Session and run creation generate an idempotency key automatically. Supply your own key when it must survive an application restart. A repeated key with the same request returns the original admission response. Changing the request or the run credentials while reusing the key returns a conflict.

The SDK never replays an agent execution on its own. Retries apply to the idempotent HTTP admission request.

Errors

API failures raise typed errors. All of them extend AxernelError.

ErrorRaised when
AuthenticationError401. The token is missing, invalid or expired.
PermissionDeniedError403. The token cannot access the resource.
NotFoundError404. The resource does not exist in this organization.
ConflictError409, 412 or 428. A revision or idempotency precondition failed.
ValidationError400, 413 or 422. The request was rejected.
RateLimitError429.
ServerErrorAny 5xx response.
APIConnectionErrorThe request never reached the API.
APITimeoutErrorThe request exceeded the client timeout.
ConfigurationErrorThe client is missing a base URL or is otherwise misconfigured.
RunFailedErrorruns.result() on a failed or timed-out run. Carries run.
RunNeedsInputErrorruns.result() on a run waiting for answers. Carries run.
RunWaitTimeoutErrorruns.wait() gave up before the run reached a terminal status.

Every APIError exposes status, code, requestId, details, and response headers.Every APIError exposes status_code, code, request_id, details, and response headers.

TypeScript
import { APIError } from "@axernel/sdk"

try {
  await axernel.agents.get(project.id, "missing")
} catch (error) {
  if (error instanceof APIError) {
    console.error(error.status, error.code, error.requestId, error.details)
  }
}
Python
from axernel import APIError

try:
    axernel.agents.get(project.id, "missing")
except APIError as error:
    print(error.status_code, error.code, error.request_id, error.details)

Resources

Both clients use the same resource names and cover the same operations.

ResourceOperations
systemhealth, readiness, publicConfig, capabilities, openAPI
authsignup, login, refresh, logout, currentUser, githubOAuthUrl
modelProviderscreate, list, iterate, get, update, delete, listPlatform, iteratePlatform, getPlatform
projectscreate, list, iterate, get, update
secretscreate, list, iterate, get, update, delete
filesupload, list, iterate, get, download, downloadTo
templatescreate, list, iterate, get, listPlatform, iteratePlatform, getPlatform
agentscreate, list, iterate, get, update
sessionscreate, list, iterate, get, close
runscreate, reply, list, iterate, get, events, wait, result
artifactsget, download, downloadTo
ResourceOperations
systemhealth, readiness, public_config, capabilities, openapi
authsignup, login, refresh, logout, current_user, github_oauth_url
model_providerscreate, list, iterate, get, update, delete, list_platform, iterate_platform, get_platform
projectscreate, list, iterate, get, update
secretscreate, list, iterate, get, update, delete
filesupload, list, iterate, get, download, download_to
templatescreate, list, iterate, get, list_platform, iterate_platform, get_platform
agentscreate, list, iterate, get, update
sessionscreate, list, iterate, get, close
runscreate, reply, list, iterate, get, events, wait, result
artifactsget, download, download_to

Current limits

The SDK does not invent client methods for backend functionality that is not in the public OpenAPI contract.

  • Skills are not implemented. There is no skills field or resource. Environment-tool declarations, attachments, instructions and MCP servers are supported, and none of them is a substitute for a versioned skill.
  • Editable workspace sources such as a repository checkout are not accepted by session creation yet. Use attachments for immutable inputs. The agent can still clone a repository with configured tools and credentials.
  • Run cancellation and mid-turn steering are not implemented.
  • Sessions are closed, not deleted.
  • Run event history is available only while the sandbox-local supervisor database remains accessible. Persist any events your application must retain.
  • Environment-tool declarations describe what the template image contains. V1 does not execute their optional checks.
  • Artifact media type is declared or inferred from the filename. V1 does not inspect the bytes to verify it.