SDK

Agents

Everything you do to a customer's agent (create it, talk to it, configure it, and clean it up) is a method on maritime.agents.

Provision (get-or-create)

provision is idempotent on externalId: first call creates the agent, later calls return the same one. That makes it safe to run on every sign-in: you never double-create, and you never have to store Maritime's agent id. Use create instead if you want a hard failure when the name already exists.

const agent = await maritime.agents.provision({
  externalId: `customer_${user.id}`,
  name: `assistant-${user.id}`,
  template: 'openclaw',              // see /docs/frameworks
  instructions: 'You are a helpful assistant.',
  idleTtlSeconds: 3600,              // 0 = always-on
})

Chat

Send a message and wait for the reply. Sleeping agents wake automatically. Pass a conversationId to keep a thread. chat resolves with { response, error? }. It does not throw on a delivery failure (a still-deploying agent, an LLM error), so check error.

const { response, error } = await maritime.agents.chat(agent.id, 'What can you do?')
if (error) throw new Error(error)

// continue a thread:
await maritime.agents.chat(agent.id, 'And after that?', { conversationId: 'thread-1' })

Per-customer secrets & config

Give each customer's agent its own credentials and config as env vars. Secrets are encrypted at rest; changes reach a running container after a reload.

await maritime.agents.setEnv(agent.id, 'ACME_API_KEY', user.acmeKey, { secret: true })
await maritime.agents.setEnv(agent.id, 'ACME_REGION', 'us-east', { secret: false })
await maritime.agents.reloadEnv(agent.id)   // push into the running container

// read them back (secret values come back masked):
for (const v of await maritime.agents.listEnv(agent.id)) {
  console.log(v.key, v.isSecret ? '(secret)' : v.value)
}

Lifecycle, logs & teardown

Agents sleep and wake on their own, but you can drive the lifecycle directly. Find an agent by your own id, read its logs, or tear it all down when a customer leaves.

const [agent] = await maritime.agents.list({ externalId: `customer_${user.id}` })

await maritime.agents.logs(agent.id, { limit: 100, level: 'error' })

await maritime.agents.sleep(agent.id)     // cheapest resting state
await maritime.agents.start(agent.id)     // wake it
await maritime.agents.restart(agent.id)

await maritime.agents.delete(agent.id)    // container + volume + network

Files & commands

Requires SDK 0.8.0 or later (maritime-sdk on npm, maritime on PyPI). On 0.7.x, call the underlying REST endpoints directly.

maritime.agents.files moves bytes both ways and manages the agent's disk; maritime.agents.exec runs a one-shot shell command. Browse and edit operations are scoped to the agent's persistent volume; take its mount from the root that list reports rather than hardcoding /data. Every call here wakes a sleeping agent; transfers cap at 100 MB per file. Uploading without destDir delivers the file as a chat attachment the agent is told about in its current conversation.

const { root, entries } = await maritime.agents.files.list(agent.id)

await maritime.agents.files.upload(agent.id, {
  content: csvBytes,
  filename: 'report.csv',
  destDir: `${root}/inbox`,   // omit destDir to deliver as a chat attachment
})

await maritime.agents.files.write(agent.id, `${root}/notes.md`, '# notes')
const bytes = await maritime.agents.files.download(agent.id, `${root}/notes.md`)

await maritime.agents.files.mkdir(agent.id, `${root}/docs`)
await maritime.agents.files.move(agent.id, `${root}/notes.md`, `${root}/docs/notes.md`)
await maritime.agents.files.delete(agent.id, `${root}/docs/notes.md`)

const { exitCode, stdout } = await maritime.agents.exec(agent.id, ['ls', '-la', root])

Move raises a conflict (409) when the destination already exists, and move/delete answer 404 for a missing source, so collisions surface as errors instead of silently doing nothing.

Scheduled wakes (inside a custom agent)

A sleeping micro-VM's timers never fire, so an agent that must act on a schedule (daily digest, follow-up reminders) publishes its schedule and lets Maritime be the alarm clock. These helpers run inside your agent image, not in your backend: they read the credentials Maritime injects into every agent and are silent no-ops anywhere else, so they're safe in local dev. Prefer nextRunAt (the next occurrence as your scheduler computed it); an entry with prompt is delivered to your POST /chat right after the wake. No SDK? Serve GET /schedules returning the same array and Maritime polls it.

import { observeScheduler, pushSchedules } from 'maritime-sdk'

// One line: every add/remove on your scheduler re-publishes the snapshot.
observeScheduler(myScheduler, {
  getSnapshot: (s) => s.jobs.map(j => ({ id: j.id, nextRunAt: j.next.toISOString() })),
})

// Or push explicitly. Send the FULL list; [] clears all synced wakes.
await pushSchedules([
  { id: 'digest', cron: '0 9 * * 1-5', tz: 'America/New_York', prompt: 'Send the digest' },
  { id: 'followup', nextRunAt: '2026-08-01T14:00:00Z' },
])

Handling errors

Every failure is a typed subclass of MaritimeError. Catch the base to catch them all, or narrow by type.

import { MaritimeConflictError, MaritimePaymentRequiredError } from 'maritime-sdk'

try {
  await maritime.agents.create({ name: 'dupe', template: 'openclaw' })
} catch (err) {
  if (err instanceof MaritimeConflictError) {
    // 409: an agent with that name already exists
  } else if (err instanceof MaritimePaymentRequiredError) {
    // 402: plan limit reached, or an add-on needs a paid plan
  } else {
    throw err
  }
}

The full error hierarchy (MaritimeAuthError, MaritimeNotFoundError, MaritimeRateLimitError, MaritimeConnectionError) is in the API reference.