@elaraai/e3-api-server
v1.0.98
Published
East Execution Engine API Server - HTTP server exposing e3-core operations
Readme
@elaraai/e3-api-server
HTTP server for e3 repositories.
Installation
npm install @elaraai/e3-api-serverOverview
REST API server exposing e3-core operations over HTTP. Uses BEAST2 binary serialization for efficient request/response encoding.
Supports two modes:
- Single-repo mode: Serve one repository, accessed via
/repos/default - Multi-repo mode: Serve multiple repositories from a directory, accessed via
/repos/:name
The server opens a repository before it serves it, as the CLI does: the one
repository when it starts, and each of several at its first request. A
repository an older release wrote is upgraded in place first, and one this e3
cannot open is refused, naming why and the fix. An upgrade waits for work
running in the repository, and no request waits with it: while a run or a task
holds a repository that owes one, a request to it is answered 503 with
Retry-After and the JSON error { error: { type:
'repository_upgrade_pending', message } }, naming the steps and the work —
except that run's cancel and poll, so the run can always be stopped. The steps
apply at the first request that finds the repository still.
CLI Usage
# Single repository mode
e3-api-server --repo /path/to/repo
e3-api-server --repo /path/to/repo --port 8080 --cors
# Multi-repository mode (serves repos from subdirectories)
e3-api-server --repos /path/to/repos-dir
# With OIDC authentication
e3-api-server --repo /path/to/repo --oidc
# Custom port and host
e3-api-server --repo /path/to/repo --port 8080 --host 0.0.0.0CLI Options
| Option | Description |
|--------|-------------|
| --repo <path> | Single repository mode - serve one repo at /repos/default |
| --repos <dir> | Multi-repo mode - serve repos from subdirectories |
| -p, --port <port> | HTTP port (default: 3000) |
| -H, --host <host> | Bind address (default: localhost) |
| --cors | Enable CORS for cross-origin requests |
| --oidc | Enable built-in OIDC authentication provider |
| --token-expiry <duration> | Access token expiry, e.g., "5s", "15m", "1h" (default: 1h) |
| --refresh-token-expiry <duration> | Refresh token expiry, e.g., "7d", "90d" (default: 90d) |
| -j, --jobs <n> | Cores: runner processes in flight at once, across every run and call the server serves and every upload it takes in (default: E3_JOBS, else the CPUs available) |
| --memory <size> | Memory those runner processes may reserve between them, as 8G or 512M (default: E3_MEMORY, else the memory available, less a reserve for e3 and the OS) |
Programmatic Usage
Single Repository (Embedded Server)
For embedding in applications like VS Code extensions:
import { createServer } from '@elaraai/e3-api-server';
// createServer is async
const server = await createServer({
singleRepoPath: '/path/to/repo',
port: 3000,
host: 'localhost',
cors: true, // Enable for webview/cross-origin access
});
await server.start();
console.log(`Server listening on http://localhost:${server.port}`);
console.log('Access repository via: /repos/default');
// Graceful shutdown
await server.stop();Multi-Repository Mode
For serving multiple repositories:
import { createServer } from '@elaraai/e3-api-server';
const server = await createServer({
reposDir: '/path/to/repos', // Each subdirectory is a repo
port: 3000,
host: 'localhost',
});
await server.start();
// Repos accessible at /repos/repo1, /repos/repo2, etc.With Authentication
import { createServer } from '@elaraai/e3-api-server';
const server = await createServer({
singleRepoPath: '/path/to/repo',
port: 3000,
oidc: {
baseUrl: 'http://localhost:3000',
tokenExpiry: '1h',
refreshTokenExpiry: '90d',
},
});
await server.start();
// OIDC endpoints available at /.well-known/*, /oauth2/*, /deviceServerConfig Options
interface ServerConfig {
// Repository mode (specify exactly one)
singleRepoPath?: string; // Single repo at /repos/default
reposDir?: string; // Multi-repo from subdirectories
// Server options
port?: number; // Default: 3000
host?: string; // Default: 'localhost'
cors?: boolean; // Enable CORS (default: false)
// Authentication (optional)
auth?: AuthConfig; // External JWT validation
oidc?: OidcConfig; // Built-in OIDC provider
// Dataset reads and uploads (optional)
pageByteBudget?: number; // Byte budget per dataset page (default: 4 MiB)
transferPartBytes?: number; // Part size for dataset uploads (default: 64 MiB)
transferCommitWaitMs?: number; // How long a commit waits before answering `processing` (default: 5000)
// Runner processes (optional)
budget?: Budget | BudgetSettings; // Cores and memory every runner the server spawns shares (default: from E3_JOBS / E3_MEMORY, else the machine)
}Mounting the routes on another host
The route factories are exported, so a host that runs e3 on its own backends
mounts them over its own seams. The repositories' routes list, create and
remove repositories through the storage backend's RepoStore, and the
repository gate, mounted ahead of every repository's routes, checks that the
repository exists and is not being removed, and opens it without holding the
request. The dataflow routes take the runner, the orchestrator that runs a
repository's dataflows, and the state store it writes: a start leaves nothing
of the run in the request's host, and a poll and a cancel read the latest run
from that store, whichever instance answers them. A host whose cancel answers
once the cancel is recorded, before the run's tasks in other processes or
machines have stopped, has its store's summary say so (cancelRequested): the
poll answers the run stopping until it has stopped, and a start, a deploy or
a workspace's removal the workspace's lock refuses meanwhile is answered
workspace_locked with a Retry-After. A run that has ended is answered
stopping too while it still holds the dataflow lock it took as it started,
which the poll asks the storage backend's lock service, so a run answered
ended has let its workspace go. The dataset transfer routes
(createTransferRoutes) take the transfer backend, whose upload store takes a
delivery in on the runner it was given: an init adopts only what takes nothing
in, and answers at once. A poll reads the latest run's summary from the store
(readLatestSummary), and its events only past the poll's cursor, a page of
its limit at a time, from the run's events, which the store keeps apart from
its state, so a store whose reads cost by the byte answers a caught-up poll
cheaply, and any poll by what it serves; the waits and
split progress it serves are the orchestrator's (getProgress), which has none
of a run another instance runs. A route answers for the repository in its URL
alone: a job, an upload or a run another repository started is not found
through it.
import { Hono } from 'hono';
import { createExecutionRoutes, createRepositoriesRoutes, createRepositoryGate } from '@elaraai/e3-api-server';
const app = new Hono();
app.use('/api/repos/:repo/*', createRepositoryGate(storage, getRepoPath));
app.route('/api/repos', createRepositoriesRoutes(storage));
app.route('/api/repos/:repo/workspaces/:ws/dataflow', createExecutionRoutes(storage, getRepoPath, {
getRunner: (repo) => runnerFor(repo),
getOrchestrator: (repo) => orchestratorFor(repo),
getStateStore: (repo) => stateStoreFor(repo),
}));A host whose requests have a time limit, which an upgrade step may outlast,
leaves the steps to a job of its own. Given applyUpgrades: false, the gate
applies none: it answers every request to a repository that owes them 503
repository_upgrade_pending — but a running dataflow's cancel and poll — until
the steps are applied, and calls onUpgradePending for each, where the host
starts its job, once. The job applies them with e3-core's
repositoryUpgradeStep, a part at a time, each running for the budget it is
given, until none is owed. A part takes the step up where the last one stopped,
whichever process ran it, and waits for work running in the repository as an
open does. No route applies a step itself: the record route (getRecord) only
reads, and refuses a repository that owes one as the gate does, gate or no.
app.use('/api/repos/:repo/*', createRepositoryGate(storage, getRepoPath, {
applyUpgrades: false,
// Each run of the job applies a part, under its time limit, and the job
// runs again while any step is owed:
// repositoryUpgradeStep(storage, getRepoPath(repo), { budgetMs: 10 * 60_000 })
onUpgradePending: (repo) => startUpgradeJob(repo),
}));A host's own routes answer their errors as these do, with the mapping every route uses, so no host keeps a copy of it that falls behind as it grows:
errorToVariantgives an e3-core error as the API'sErrorType, whichsendErroranswers in BEAST2;errorToHttpStatusandsendJsonErroranswer it as JSON, as the gate answers its refusals;sendUpgradePendinganswers a repository that owes upgrades as the gate does.
A host's own routes over records answer as the record and dataset routes do:
mutationResultOfgives the outcome of a record operation as theMutationResultTypevalue the mutation and compact routes answer with, such as a rollback's through e3-core'srecordSystemCommit;getValuePageanswers a window of a collection the host names by hash, such as a record's state at a past commit, with the body, theX-*headers and the refusals of a dataset's page (getDatasetPage).
Each is exported from the root and the portable entries, and each handler from
./handlers too. e3-api-client reads what they answer: parsePage a page,
parseErrorBody a JSON refusal, and objectGet and collectionGetStream a
value by its hash.
A host whose requests have a time limit gives it to the routes that run a
program for a request, so that each answers its typed outcome before the host
cuts it off: the function and one-shot routes take syncDeadlineMs, and so do
the record routes, whose mutation and compaction answer timed_out or
conflict 2 s under it. The record routes' historyLimit is how many commits
a history request that names no limit is answered with; the client pages on
from the last one's parent. Unset, as on this server, neither is bounded. A
limit no request could meet is refused when the routes are mounted.
app.route('/api/repos/:repo/workspaces/:ws/records', createWorkspaceRecordRoutes(storage, getRepoPath, getRunner, {
syncDeadlineMs: 29_000, // under a 30 s gateway
historyLimit: 1_000,
}));A dataset page or key search pinned to the value's hash (hash=) never
changes, so it is answered Cache-Control: private, max-age=31536000,
immutable: the caller's own cache keeps it for good, and no cache between
keeps a workspace's rows for whoever asks next. A host whose data any caller may
read lets those caches keep it too, a CDN's among them, with the dataset
routes' cache: 'public'; getDatasetPage, getValuePage and
findDatasetKey take the same. An answer not pinned is no-store, and so is a
batch read's (findKeys, rows, runs, place, within), a POST whose body
names what it reads.
A batch read is a POST to a dataset's own path, as an upload's init is to its
/upload: a host mounts the dataset routes after the transfer routes, at the
same prefix, and the transfer routes pass on to them a POST that is no upload,
and every POST that names a batch, whatever its path ends in — so a dataset
named upload is read in batches as any other.
app.route('/api/repos/:repo/workspaces/:ws/datasets', createDatasetRoutes(storage, getRepoPath, transferBackend, {
cache: 'public', // every caller may read every workspace this host serves
}));API Endpoints
All endpoints are prefixed with /api/repos/:repo where :repo is:
defaultin single-repo mode- The repository name in multi-repo mode
Repository
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | /api/repos | List available repositories (multi-repo mode) |
| PUT | /api/repos/:repo | Create repository (multi-repo mode) |
| DELETE | /api/repos/:repo | Remove repository (multi-repo mode): marked as being removed first, so a request to it is refused from then on. This server removes it within the request, and answers 200; a host whose removal outlasts a request answers 202 Accepted, with a Location naming the route that says how the removal goes (RepositoryRemovalType: removing, removed or failed) and a Retry-After, which e3-api-client's repoRemove follows until the repository is gone |
| GET | /api/repos/:repo/status | Repository status (counts) |
| GET | /api/repos/:repo/record | The repository's record: the release that last wrote it, and the upgrades it has had. A read, which applies none it owes: without the gate ahead of it, a repository that owes one is refused as the gate refuses it, 503 repository_upgrade_pending with Retry-After |
| POST | /api/repos/:repo/gc | Start garbage collection (async) |
| GET | /api/repos/:repo/gc/:id | Get GC status |
Packages
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | /api/repos/:repo/packages | List all packages |
| GET | /api/repos/:repo/packages/:name/:version | Get package details |
| POST | /api/repos/:repo/import | Start importing a package zip, as a job: answers the job's id and where to upload the zip |
| POST | /api/repos/:repo/import/:id | Import the uploaded zip |
| GET | /api/repos/:repo/import/:id | Poll an import job |
| POST | /api/repos/:repo/packages/:name/:version/export | Start exporting a package as a zip, as a job: answers the job's id |
| GET | /api/repos/:repo/export/:id | Poll an export job: once it completes, where to download the zip |
| DELETE | /api/repos/:repo/packages/:name/:version | Remove package |
Workspaces
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | /api/repos/:repo/workspaces | List all workspaces |
| POST | /api/repos/:repo/workspaces | Create workspace |
| POST | /api/repos/:repo/workspaces/:ws/copy | Copy the workspace to the one the body names (to), made or replaced whole: it becomes this workspace as it is now. Answers the target as the list gives it |
| GET | /api/repos/:repo/workspaces/:ws | Get workspace info |
| GET | /api/repos/:repo/workspaces/:ws/status | Get workspace status (datasets, tasks, summary). With ?path= (repeated, each a dataset's keypath, .inputs.x), only those datasets and the tasks producing them, each as the whole status gives it: a path that names no dataset is left out, and one that is no keypath is refused 400 bad_request. The answer carries an ETag; a request whose If-None-Match names it is answered 304 Not Modified with no body, nothing of the workspace read but its revision |
| POST | /api/repos/:repo/workspaces/:ws/deploy | Start deploying a package to the workspace, as a job: answers the job's id |
| GET | /api/repos/:repo/workspaces/:ws/deploy/:id | Poll a deploy job: processing with how far it has got, what the deploy did for each record, index and input, or why it failed |
| GET | /api/repos/:repo/workspaces/:ws/lock | What holds the workspace exclusively, and how far it says it has got; none when nothing does |
| DELETE | /api/repos/:repo/workspaces/:ws | Remove workspace |
| POST | /api/repos/:repo/workspaces/:ws/export | Start exporting the workspace as a package zip, as a job polled at /api/repos/:repo/export/:id |
A deploy that migrates a record, or builds an index over one, takes as long as
the record is large, so it runs as a job, on the runner the server runs every
record operation on. Its request names the package, what the deploy does with a
record it cannot keep as it is (schema: migrate, fail or reset), what
it does with an input someone set (inputs: reset, which gives it the
package's value, or keep-edited, which keeps it while its type is the
package's), whether it may drop a record the package no longer declares
(allowDropRecords), and whether it only says what it would do (plan). The
job's result says what it did with each input: gave it the package's value,
kept it, reset it and why, took it from its file, or dropped it.
While it runs, the job's processing says how far it has got: each file
source's step, and each record's (waiting, migrating step n of m, building
index n of m, done). The deploy reports the same through the workspace's lock,
which /lock answers. A workspace deployed for the first time has no status
until its deploy ends, so /lock is where another client, such as e3-ui's TUI,
follows that deploy.
A status is answered conditionally. Its ETag names the workspace's status
revision, which moves with every write of one of its dataset refs, of its
record, of any run's record of the repository (a split task's unit changes
nothing a status answers, so its write leaves it), and with its lock: a poll
that sends the tag back as If-None-Match is answered 304 Not Modified
while nothing has moved, which reads the revision and nothing else of the
workspace. The revision is read before the status is computed, so a tag is
never newer than the answer it names. Both answers are Cache-Control:
no-store: no cache between keeps a status, or answers a poll in the server's
place; with --cors the ETag is exposed, so a browser client reads it. The
tag does not name the runner's view of an execution recorded running, so a
client that must notice a runner gone asks unconditionally now and then:
e3-api-client's workspaceStatusSince asks conditionally, and
e3-ui-components' dataset cache asks unconditionally once its last full answer
is thirty seconds old, and after a fetch it made failed, a new watch or a
refresh.
A copy writes refs only — within a repository every value is shared by its
hash — so it costs what the workspace has of datasets, never what they weigh,
and nothing the target holds needs to run again. A target that exists loses its
refs and its runs. The copy holds the workspace copied shared, so a run in it
goes on, and the target exclusively, as a deploy does: a copy onto a workspace a
dataflow runs in, or that something else holds, is refused workspace_locked,
and a copy onto itself invalid_name.
Datasets
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | /api/repos/:repo/workspaces/:ws/datasets | List root datasets |
| GET | /api/repos/:repo/workspaces/:ws/datasets/*path | Get dataset value (BEAST2); a value over 1 MB that is not a collection answers JSON { url } to download it from |
| GET | /api/repos/:repo/workspaces/:ws/datasets/*path?segments=true | A collection answers JSON { manifest }, the manifest to download it by; any other value as above |
| GET | /api/repos/:repo/workspaces/:ws/datasets/*path?page=true | A window of a collection (offset, limit, or a writer's segment), or of one of a record's indexes (index, join), as BEAST2 of its type, placed by its X-* headers; pinned with hash |
| GET | /api/repos/:repo/workspaces/:ws/datasets/*path?find=true | Locate a key (key), a prefix (prefix), leading fields (field, repeated) or a range (from, to) in a Set's or a Dict's order, or an index's (index): JSON { found, row, count }; pinned with hash |
| POST | /api/repos/:repo/workspaces/:ws/datasets/*path?findKeys=true | Several key searches of one collection or index (index), { queries: [...] }, each in one of find's forms, at most 10,000: JSON { results: [{ found, row, count }, …] }, in order; pinned with hash |
| POST | /api/repos/:repo/workspaces/:ws/datasets/*path?rows=true | The rows of several keys of a Dict, { keys: [<.east>, …] }, at most 10,000 — or of the keys the entries of some runs of another record's index name, { keysOf: { path: ".<record>", index, hash, runs: [[row, count], …] } }, which the server reads from the index, so none is sent: BEAST2 of datasetRowsType(K, V) — each key, its row or none, and its position in the Dict's order, in the order asked or the runs' — for the first X-Page-Count keys the page budget and a page's 10,000 entries reach (read by runs, of the X-Run-Entries the runs hold); pinned with hash, the runs with keysOf.hash, a stale one refused 409 runs_hash_mismatch naming the record's content (X-Content-SHA256) |
| POST | /api/repos/:repo/workspaces/:ws/datasets/*path?runs=true&hash= | The entries of several runs of a collection's rows, or of an index's (index, join), { runs: [[row, count], …] }, ascending and disjoint, at most 10,000: one window of the dataset's type, or the index's window type, holding the first X-Page-Count of the runs' X-Run-Entries entries a page's bounds reach |
| POST | /api/repos/:repo/workspaces/:ws/datasets/*path?place=true&hash= | Where a narrowing's rows sit in a record: { index, runs: [[row, count], …], offset?, limit? }, the runs of one of its indexes, at most 10,000, a window of their entries from offset: JSON { segments: [{ segment, offset, count, rows }], placed } — each record segment the window's entries fall in, its first row, its row count and how many of them it holds — for the first placed entries a page's bounds reach; X-Total-Elements the record's rows, X-Run-Entries the runs' |
| POST | /api/repos/:repo/workspaces/:ws/datasets/*path?within=true&hash= | Which of some index entries a narrowing's runs hold: { index, runs: [[row, count], …], keys: ["(ik=…, k=…)", …] }, each key an entry's index key beside its row's key, at most 10,000 runs and 10,000 keys: JSON { within: [boolean, …] }, in order |
| PUT | /api/repos/:repo/workspaces/:ws/datasets/*path | Set dataset value (BEAST2) |
A collection is streamed as the splice of its segments. A client whose host
buffers responses downloads the manifest, the header it names and its segments
through the objects route and splices them itself, as e3-api-client's
datasetGet does; see
design/e3-data-architecture.md.
A batch's body is at most 16 MiB (DATASET_BATCH_BODY_MAX): some 1.6 KB of
.east literals for each of the most queries, keys or runs a batch names. A
larger one is refused 413 payload_too_large as soon as its Content-Length
says so, or as its bytes pass the cap, and the rest is never read, so no
request makes the server hold more of its body than that.
A window's reads are a few requests, never one a search, a run or a row: its
key searches in one findKeys, the runs they select in one runs, the rows of
the keys it names in one rows, and another record's rows for its runs — a
drawing's details — in one rows by keysOf, the keys read from the index
where the runs' entries are kept, never sent. Each batch places its bounds,
keys or rows by the manifest's fences and counts, which reads nothing, then
reads every segment it touches once, several at a time. The server keeps decoded segments
by content hash, bounded by their decoded weight (64 MiB), and every page,
search and batch reads through them — so a window read again, or a pan over
the same segments, decodes none. A value read whole is streamed as its
segments' bytes, and decodes none of them.
A summary index's entry stands for every row of its key — its key Null, its
value the rows' totals — so it names no row to join: a page or runs read of
one with join is refused 400 bad_request, naming the index, and so are a
rows read by its runs (keysOf), a place and a within, which have no row
to place or to answer of. A read by runs also refuses an index whose entries
name keys of another type than the Dict's, East deciding.
Where a narrowing's rows sit among a record's is two reads of its index alone,
never of a record segment. place reads the window's entries from the
index's own segments and places each entry's row in the record segment that
holds it by the record's fences, which its manifest carries: a narrowing's runs
name each row once, so a segment's counts over every window add up to the
narrowing's rows in it. within reads each run's first and last entries — at
most two index segments a run — and places every key it is asked of among them
by the index entry key's order. Neither evaluates anything over a row: the index
holds what its maintained key functions emitted, and both use only that.
Objects
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | /api/repos/:repo/objects/:hash | An object's bytes; one over 1 MB answers JSON { url } to download it from; a hash that is no SHA-256 in lowercase hex is refused 400 invalid_name |
| GET | /api/downloads/:id | A download a { url } answer names (no Authorization) |
Dataset transfer
Values too large to PUT inline are staged in parts and committed. The init
and the commit name the protocol version with ?protocol=3, and the client's
release with &release=, which decides nothing. A request of another version,
or none, is refused, naming the server's release, the request's, and the fix.
The full protocol is in
design/e3-data-architecture.md.
| Method | Endpoint | Description |
|--------|----------|-------------|
| POST | /api/repos/:repo/workspaces/:ws/datasets/*path/upload | Start an upload: completed (already stored) or upload_parts |
| GET | /api/repos/:repo/workspaces/:ws/datasets/*path/upload/:id/parts/:n | URL and headers for part n |
| POST | /api/repos/:repo/workspaces/:ws/datasets/*path/upload/:id | Commit: completed, error, or processing, with how far it has taken the file in once the store has said |
| GET | /api/repos/:repo/workspaces/:ws/datasets/*path/upload/:id | Poll a commit |
| PUT | /api/uploads/:id | A package zip being imported (no Authorization) |
| PUT | /api/uploads/:id/parts/:n | Part n of a dataset upload (no Authorization) |
A commit takes a collection in by intake units on the repository's runner — a
piece of its segments each, as many at once as the server's budget allows — and
its processing says how many pieces are in. An upload stopped part way and
sent again takes up from the pieces it finished.
An init whose bytes the store already knows answers completed when adopting
them takes nothing in: the manifest an earlier delivery of them became, or an
object of a value that is not a collection. A collection the store holds whole,
which no finished intake has taken in, is uploaded and committed as any other,
and its commit takes it in.
Tasks
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | /api/repos/:repo/workspaces/:ws/tasks | List tasks |
| GET | /api/repos/:repo/workspaces/:ws/tasks/:task | Get task details |
Functions and one-shot
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | /api/repos/:repo/packages/:pkg/:version/functions | A package's functions and their signatures |
| GET | /api/repos/:repo/packages/:pkg/:version/functions/:fn | One function's signature |
| POST | /api/repos/:repo/packages/:pkg/:version/functions/:fn | Call a package's function with values: its result, inline. A dataset argument is invalid here, with no workspace to read it from |
| GET | /api/repos/:repo/workspaces/:ws/functions | The deployed package's functions |
| GET | /api/repos/:repo/workspaces/:ws/functions/:fn | One function's signature |
| POST | /api/repos/:repo/workspaces/:ws/functions/:fn | Call one of them: each argument a value, or one of the workspace's datasets, pinned at its hash when the call starts; the result names the datasets it read (inputs) |
| POST | /api/repos/:repo/workspaces/:ws/one-shot | Run a caller's program over the workspace's datasets: its result, naming the datasets it read (inputs) |
| POST | /api/repos/:repo/workspaces/:ws/one-shot/split | Launch a split call — a caller's program over a dataset's pieces — as a job: answers the job's id; with ?explain=1 the job plans the pieces the call would run, and runs nothing |
| GET | /api/repos/:repo/workspaces/:ws/one-shot/split/:id | Poll a split call: how far it has got, its result, the pieces an explain planned, or why it failed |
What a caller may run through one-shot is the grant the host gives it
(OneShotAccess). This server's, with auth, gives an elevated role (admin,
owner) any program, and any other identity a platform-free one: a stock
runner given no platform, and a program that calls none. Without auth, a
single-tenant server, it gives any.
A call to a function runs it on the runner its author chose, for any caller.
A call that names a runner of its own is held to the same grant, which the
function routes take as access: a runner on the custom runtime is refused,
invalid, whoever names it, and one that loads a platform package the
function's own runner does not is refused permission_denied (path runner)
without the elevated grant. A host that mounts the function routes without
access refuses every caller such a runner.
A split call runs as a split task, so each piece and merge is cached: a relaunch is served from the cache, and one after an append reruns only the pieces the append reached. Its timeout is one budget for the whole job, counted from its launch. A call is polled only through the repository and workspace that launched it, and a caller with a platform-free grant polls only a platform-free call, or one such a caller launched.
Records
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | /api/repos/:repo/workspaces/:ws/records/:rec | The record's mutations, indexes and migrations, with their types: whether each index is a summary and whether it is built for the head's state, and when each migration was applied and by which commit, read from the record's ref and state, never its history |
| GET | /api/repos/:repo/workspaces/:ws/records/:rec/history | Its commits, newest first: ?limit= of them from ?from=, the head when absent; with no limit, the whole chain, or the host's page (historyLimit). Each previews its arguments: the arguments' object, and each argument's type, size and East text, cut at 256 characters, the text of one over 64 KiB left out, and no argument of an object over 1 MiB, which the objects route serves whole |
| POST | /api/repos/:repo/workspaces/:ws/records/:rec/mutations/:mut | Apply a mutation: committed, invalid, failed, timed_out or conflict; with an Idempotency-Key, a retry answers the first call's commit |
| POST | /api/repos/:repo/workspaces/:ws/records/:rec/compact | Collapse the history to a $compact root, the state kept (an elevated role when auth is on) |
Execution
| Method | Endpoint | Description |
|--------|----------|-------------|
| POST | /api/repos/:repo/workspaces/:ws/dataflow | Start a run of the dataflow (answers 202 once it has started) |
| GET | /api/repos/:repo/workspaces/:ws/dataflow | Get workspace status (for polling), with the ETag and 304 Not Modified of /workspaces/:ws/status |
| GET | /api/repos/:repo/workspaces/:ws/dataflow/execution | The latest run's state, its id (runId) and the sequence number of its last event (lastSeq), and its events past the poll's cursor (since, the nextSeq the poll before answered; at most limit of them, and never more than 1,000, the default: a nextSeq before lastSeq left some for the next poll), with the server's budget in use; while the run is in flight, the tasks and units waiting for room and each split task's progress, as the orchestrator running it answers them. A run asked to stop that has not yet stopped is stopping, and so is one that has ended — its summary given — until it has let its workspace go: a run answered completed, failed or aborted holds nothing, so a gc, a deploy or a run asked of next takes the workspace |
| GET | /api/repos/:repo/workspaces/:ws/dataflow/budget | The budget a run gets: the server's cores and memory, and what its runners hold now (none from a host whose runners hold none) |
| POST | /api/repos/:repo/workspaces/:ws/dataflow/cancel | Cancel the run in progress: this server answers once the run has ended; a host whose run's tasks run elsewhere may answer once the cancel is recorded, the poll answering the run stopping until it has stopped. e3-api-client's dataflowCancel follows the run until it has |
| GET | /api/repos/:repo/workspaces/:ws/dataflow/graph | Get dependency graph |
| GET | /api/repos/:repo/workspaces/:ws/dataflow/logs/:task | Read task logs |
Request/Response Format
All requests and responses use BEAST2 binary encoding with Content-Type: application/beast2.
Response bodies are wrapped in a variant type:
{ type: 'success', value: <result> }- Operation succeeded{ type: 'error', value: <error> }- Operation failed
Error variants include:
invalid_name- A name, hash or id is not of its form: its kind (a workspace's name, an object's hash, a run's id, …), the value, and why, whichever backend the server runs overworkspace_not_found- Workspace doesn't existworkspace_not_deployed- No package deployed to workspaceworkspace_locked- Workspace is locked by another process; with aRetry-Afterwhen the holder is a run that is stopping — asked to stop and not yet stopped, or ended and still letting the workspace go — which e3-api-client's start, deploy and workspace removal wait out and ask againpackage_not_found- Package doesn't existpackage_exists- Package already existsdataset_not_found- Dataset path doesn't existtask_not_found- Task doesn't existpermission_denied- The caller's grant does not run the request: a one-shot or split call (pathone-shot), or a function call's runner (pathrunner)internal- Internal server error
Using with e3-api-client
import { workspaceList, workspaceStatus, datasetGet } from '@elaraai/e3-api-client';
const baseUrl = 'http://localhost:3000';
const repo = 'default'; // In single-repo mode
const options = { token: '' }; // Empty token if no auth configured
// List workspaces
const workspaces = await workspaceList(baseUrl, repo, options);
// Get workspace status
const status = await workspaceStatus(baseUrl, repo, 'my-workspace', options);
// Get dataset value
const path = [{ value: 'inputs' }, { value: 'data' }];
const data = await datasetGet(baseUrl, repo, 'my-workspace', path, options);Claude Code plugin
The East ecosystem also ships a Claude Code plugin — East language skills, example search, and preemptive diagnostics for East code — installed separately from the elaraai marketplace:
# Inside Claude Code
/plugin marketplace add elaraai/east-workspace
/plugin install east@elaraai# From a terminal
claude plugin marketplace add elaraai/east-workspace
claude plugin install east@elaraaiLicense
BSL 1.1. See LICENSE.md.
Ecosystem
East: Statically typed, expression-based language with serializable IR. Run portable logic across TypeScript, Python, C, and other runtimes.
- @elaraai/east: Core language SDK with type system, expressions, and reference JS compiler
East Node: Node.js platform functions for I/O, databases, and system operations.
- @elaraai/east-node-std: Console, FileSystem, Fetch, Crypto, Time, Path, Random
- @elaraai/east-node-io: SQLite, PostgreSQL, MySQL, MongoDB, Redis, S3, FTP, SFTP, XLSX, XML, compression
- @elaraai/east-node-cli: CLI for running East IR programs in Node.js
East C: C11 native runtime for executing East IR. Distributed via npm (launcher + per-platform optional dependencies) and as tarballs on each GitHub Release.
- @elaraai/east-c-cli: npm launcher — installs the matching native binary as an optional dependency
east-c: Core runtime — type system, IR interpreter, builtins, serialization (Beast2, JSON, CSV, East text)east-c-std: Console, FileSystem, Fetch, Crypto, Time, Path, Randomeast-c-cli: CLI for running East IR programs natively
East Python: Python runtime, standard platform, I/O, and data-science platform functions. Published to PyPI.
- east-py: Core Python runtime — type system, IR compiler, 212+ builtins, Cython-accelerated hot paths
- east-py-std: Console, FileSystem, Fetch, Crypto, Time, Path, Random
- east-py-io: SQLite, PostgreSQL, MySQL, MongoDB, Redis, S3, FTP, SFTP, XLSX, XML, compression
- east-py-cli: CLI for running East IR programs in Python
- east-py-datascience (PyPI) + @elaraai/east-py-datascience (npm): Optimization (MADS, Optuna, ALNS, GoogleOR), ML (XGBoost, LightGBM, NGBoost, PyTorch, Lightning, GP), Bayesian inference (PyMC), explainability (SHAP), conformal prediction (MAPIE)
East UI: Typed UI component definitions and React renderer, plus VS Code preview.
- @elaraai/east-ui: 50+ typed UI components for layouts, forms, charts, tables, dialogs
- @elaraai/east-ui-components: React renderer with Chakra UI v3 styling
- @elaraai/e3-ui: e3 + UI bridge — Data bindings,
e3.ui()task, manifest - @elaraai/e3-ui-components: React Query hooks and preview components for the e3 API
- east-ui-preview: VS Code extension for live East UI component preview
e3 — East Execution Engine: Durable execution engine for running East pipelines at scale. Git-like content-addressable storage, automatic memoization, reactive dataflow, real-time monitoring.
- @elaraai/e3: SDK for authoring e3 packages with typed tasks and pipelines
- @elaraai/e3-core: Object store, dataflow orchestrator, execution state
- @elaraai/e3-types: Shared type definitions for e3 packages
- @elaraai/e3-cli:
e3 repo,e3 package,e3 workspace,e3 dataflow run,e3 watch,e3 task logscommands - @elaraai/e3-api-client: HTTP client for remote e3 repositories
- @elaraai/e3-api-server: REST API server for e3 repositories
- @elaraai/e3-api-tests: Shared API compliance test suites
Links
About Elara
East is developed by Elara AI Pty Ltd, an AI-powered platform that creates economic digital twins of businesses that optimize performance. Elara combines business objectives, decisions and data to help organizations make data-driven decisions across operations, purchasing, sales and customer engagement, and project and investment planning. East powers the computational layer of Elara solutions, enabling the expression of complex business logic and data in a simple, type-safe and portable language.
Developed by Elara AI Pty Ltd.
Developed by Elara AI Pty Ltd
