Skip to content
PricingBlog
✨ Markdown

Programmatic runtime client

createRuntimeServerClient() is the lower-level HTTP client for the Electric Agents server. Handler code should usually use ctx.spawn(), ctx.send(), ctx.observe(), and ctx.mkdb() instead. Use this client from application services, tests, CLIs, and integration code that needs to manage entities from outside a handler.

ts
import { createRuntimeServerClient } from "@electric-ax/agents-runtime"

const client = createRuntimeServerClient({
  baseUrl: "http://localhost:4437",
})

Config

ts
interface RuntimeServerClientConfig {
  baseUrl: string
  fetch?: typeof globalThis.fetch
  headers?: HeadersProvider
  writeTokenHeader?: ClaimTokenHeader
  track?: <T>(promise: Promise<T>) => Promise<T>
  principalKey?: string
}
FieldDescription
baseUrlBase URL for the Electric Agents server.
fetchOptional fetch implementation, useful in tests or non-standard runtimes.
headersStatic or async headers added to requests, useful for auth or tenant scope.
writeTokenHeaderHeader transport for claim-scoped write tokens: authorization, electric-claim-token, or both.
trackOptional wrapper for all requests, useful for telemetry or pending state.
principalKeyPrincipal key sent as Electric-Principal on requests.

Entity Lifecycle

spawnEntity

ts
const info = await client.spawnEntity({
  type: "horton",
  id: "onboarding",
  args: { timezone: "Europe/London" },
  initialMessage: "Help me get started.",
  tags: { project: "docs" },
  sandbox: { profile: "local", scope: "entity" },
})

console.log(info.entityUrl) // "/horton/onboarding"

spawnEntity() is idempotent for an existing /{type}/{id} URL: if the server reports a conflict and returns entity details, the client returns that entity info.

ts
interface SpawnEntityOptions {
  type: string
  id: string
  args?: Record<string, unknown>
  parentUrl?: string
  initialMessage?: unknown
  initialMessageType?: string
  tags?: Record<string, string>
  sandbox?: {
    profile?: string
    key?: string
    scope?: "entity" | "wake"
    persistent?: boolean
    owner?: boolean
    inherit?: boolean
  }
  dispatch_policy?: DispatchPolicy
  wake?: {
    subscriberUrl: string
    condition:
      | "runFinished"
      | {
          on: "change"
          collections?: string[]
          ops?: Array<"insert" | "update" | "delete">
        }
    debounceMs?: number
    timeoutMs?: number
    includeResponse?: boolean
    manifestKey?: string
  }
}

forkEntity

forkEntity() wraps POST /_electric/entities/<type>/<id>/fork and creates a new entity from a source entity's latest completed run:

ts
const fork = await client.forkEntity({
  sourceEntityUrl: "/horton/onboarding",
  instanceId: "onboarding-variant",
  initialMessage: { text: "Try a different approach." },
  tags: { branch: "variant" },
})

console.log(fork.entityUrl)
ts
interface ForkEntityOptions {
  sourceEntityUrl: string
  instanceId?: string
  parent?: string
  wake?: {
    subscriberUrl: string
    condition: RegisterWakeOptions["condition"]
    debounceMs?: number
    timeoutMs?: number
    includeResponse?: boolean
    manifestKey?: string
  }
  initialMessage?: unknown
  tags?: Record<string, string>
}

initialMessage is sent after the fork has been created and dispatch subscriptions are linked, so a partial failure can leave an idle fork. Handler code should prefer ctx.fork() or ctx.forkSelf().

getEntity

ts
const info = await client.getEntity("/horton/onboarding")
// { entityUrl, entityType, streamPath }

deleteEntity

ts
await client.deleteEntity("/horton/onboarding")

Deleting an already-missing entity is treated as success.

Messages

ts
await client.sendEntityMessage({
  targetUrl: "/horton/onboarding",
  payload: "What changed since last time?",
  type: "user_message",
  mode: "queued",
})
ts
interface SendEntityMessageOptions {
  targetUrl: string
  payload: unknown
  type?: string
  afterMs?: number
  mode?: "immediate" | "queued" | "paused" | "steer"
  position?: string
  fromPrincipal?: string
  fromAgent?: string
  writeToken?: string
}

afterMs asks the server to deliver the message later. mode controls how the server queues or applies the message. fromPrincipal, fromAgent, and writeToken are advanced fields for claim-scoped runtime writes.

Signals

Send lifecycle signals to an entity:

ts
await client.signalEntity({
  entityUrl: "/horton/onboarding",
  signal: "SIGINT",
  reason: "User stopped the current run",
})

deleteEntity() sends SIGKILL and treats an already-missing entity as success:

ts
await client.deleteEntity("/horton/onboarding")

Attachments

Attachments are uploaded through entity routes, stored in private attachment streams, and referenced by manifest entries:

ts
const { attachment } = await client.createAttachment({
  entityUrl: "/horton/onboarding",
  attachment: {
    bytes: imageBytes,
    mimeType: "image/png",
    filename: "diagram.png",
    subject: { type: "inbox", key: "message-1" },
    role: "input",
  },
})

const bytes = await client.readAttachment({
  entityUrl: "/horton/onboarding",
  id: attachment.id,
})

Shared State

ts
const streamPath = await client.ensureSharedStateStream("research-123")
// "/_electric/shared-state/research-123"

const samePath = client.getSharedStateStreamPath("research-123")

Use ensureSharedStateStream() when app code needs to create a shared-state stream before entities connect to it.

Wakes and Sources

registerWake

registerWake() creates a wake subscription from one source stream to a subscriber entity.

ts
await client.registerWake({
  subscriberUrl: "/coordinator/research",
  sourceUrl: "/worker/analyst/main",
  condition: "runFinished",
  includeResponse: true,
})

For change wakes:

ts
await client.registerWake({
  subscriberUrl: "/monitor/main",
  sourceUrl: "/horton/onboarding/main",
  condition: {
    on: "change",
    collections: ["runs", "texts"],
    ops: ["insert", "update"],
  },
  debounceMs: 250,
})

ensureCronStream

ts
const streamUrl = await client.ensureCronStream(
  "0 9 * * *",
  "Europe/London"
)

ensureEntitiesMembershipStream

ts
const source = await client.ensureEntitiesMembershipStream({ project: "docs" })
// { streamUrl, sourceRef }

This is the lower-level operation behind observing entities({ tags }).

registerPgSyncSource

ts
const source = await client.registerPgSyncSource({
  url: "http://localhost:3000/v1/shape",
  table: "todos",
  where: "project_id = $1",
  params: ["docs"],
})
// { streamUrl, sourceRef }

This is the lower-level operation behind observing pgSync({ url, table, where, params }) sources. The server turns the Postgres shape into an Electric Agents observation stream.

Remove an entity's pg-sync observation by source reference:

ts
await client.removePgSyncObservation({
  entityUrl: "/horton/onboarding",
  sourceRef: source.sourceRef,
})

Webhook sources

Webhook-source APIs expose webhook-backed feeds that agents can subscribe to:

ts
const sources = await client.listWebhookSources()

await client.subscribeToWebhookSource({
  entityUrl: "/horton/onboarding",
  id: "github-main",
  webhookKey: "github",
  bucketKey: "repo",
  params: { repo: "electric-sql/electric" },
  lifetime: { kind: "until_entity_stopped" },
})

await client.unsubscribeFromWebhookSource({
  entityUrl: "/horton/onboarding",
  id: "github-main",
})

Schedules

Schedules are stored on an entity manifest and return the write transaction id.

ts
await client.upsertCronSchedule({
  entityUrl: "/horton/onboarding",
  id: "daily-checkin",
  expression: "0 9 * * *",
  timezone: "Europe/London",
  payload: "Run the daily check-in.",
})

await client.upsertFutureSendSchedule({
  entityUrl: "/horton/onboarding",
  id: "follow-up",
  fireAt: new Date(Date.now() + 60_000).toISOString(),
  payload: "Follow up now.",
})

await client.deleteSchedule({
  entityUrl: "/horton/onboarding",
  id: "follow-up",
})

Tags

setTag() and deleteTag() are primarily for handler/runtime-owned flows that already hold the current claim-scoped write token. External clients should prefer sendEntityMessage() and write only to an entity's inbox rather than writing entity state directly.

ts
await client.setTag("/horton/onboarding", "title", "Onboarding", writeToken)
await client.deleteTag("/horton/onboarding", "title", writeToken)

Choosing a Client

APIUse when
ctx.spawn/send/observeYou are inside an entity handler.
createAgentsClient()You need to observe streams and drive UI state.
createRuntimeServerClient()You need to manage entities, messages, wakes, schedules, or tags externally.
electric-ax/entity-stream-dbYou need the CLI-style entity stream loader with close().