npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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-server

Overview

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.0

CLI 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/*, /device

ServerConfig 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:

  • errorToVariant gives an e3-core error as the API's ErrorType, which sendError answers in BEAST2;
  • errorToHttpStatus and sendJsonError answer it as JSON, as the gate answers its refusals;
  • sendUpgradePending answers a repository that owes upgrades as the gate does.

A host's own routes over records answer as the record and dataset routes do:

  • mutationResultOf gives the outcome of a record operation as the MutationResultType value the mutation and compact routes answer with, such as a rollback's through e3-core's recordSystemCommit;
  • getValuePage answers a window of a collection the host names by hash, such as a record's state at a past commit, with the body, the X-* 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:

  • default in 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 over
  • workspace_not_found - Workspace doesn't exist
  • workspace_not_deployed - No package deployed to workspace
  • workspace_locked - Workspace is locked by another process; with a Retry-After when 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 again
  • package_not_found - Package doesn't exist
  • package_exists - Package already exists
  • dataset_not_found - Dataset path doesn't exist
  • task_not_found - Task doesn't exist
  • permission_denied - The caller's grant does not run the request: a one-shot or split call (path one-shot), or a function call's runner (path runner)
  • 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@elaraai

License

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.

  • 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, Random
    • east-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.

  • 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.

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