@elementumai/edk
v0.9.6
Published
Elementum Development Kit — statically typed SDK for building Elementum apps, automations, and agents
Readme
@elementumai/edk
Statically typed TypeScript SDK for authoring Elementum apps, elements,
automations, agents, skills, search tables, and related configuration as
code. @elementumai/edk is SDK-only; the standalone elementum CLI is
distributed separately as part of the signed toolchain.
This repository is the canonical source for the public SDK, the standalone
elementum CLI, the OpenTofu provider under provider/, and the temporary
private bridge under cmd/elementum/. The installed toolchain manages
the bridge and OpenTofu; neither is a separate public executable or checkout.
Installation
Install the authenticated macOS or Linux x64 toolchain:
curl -fsSL https://install.elementum.tools | sh
elementum initThe installer places elementum in ~/.elementum/bin and adds that directory
to PATH via your shell profile (~/.zprofile for zsh, otherwise the applicable
profile file). The current shell won't see it until the profile reloads.
Install the SDK in the package workspace that contains your Elementum org folders:
npm install @elementumai/edkUse Node 24 or newer. The npm package is the authoring SDK; the only public
executable is elementum.
Platform support
The authenticated elementum toolchain targets macOS Apple Silicon
(darwin-arm64), macOS Intel (darwin-x64), and Linux x64
(linux-x64, glibc). Linux distribution work is tracked in
#186. The authoring SDK
(@elementumai/edk) remains pure TypeScript and runs anywhere Node 24+ runs.
Authentication and profiles
The SDK is credential-free and never calls the platform. Authentication and all platform I/O are owned by the managed private bridge:
elementum auth login --profile <profile>
elementum --profile=<profile> auth statusA profile selects the organization, instance, optional organization environment, credentials, and custom endpoints. Pass it explicitly before any command when reproducibility matters:
elementum --profile=<profile> pull org --data-only
elementum --profile=<profile> planFor plan and apply, --profile is optional. The CLI resolves the marked org
root from the explicit target or current directory, including targets nested
under apps/, elements/, or an individual authored file. It reads
<instance>/<organization> from that org root's path and selects the one saved
profile with the same identity. It does not infer tenant identity from shell
variables.
An explicit --profile overrides filesystem selection. The existing safety
check still rejects that profile if its instance or organization differs from
the workspace path. If no saved profile matches, or multiple profiles have the
same filesystem identity, the command fails before planning with the workspace
identity and tells you to pass --profile explicitly. Commands that bootstrap a
workspace should use an explicit profile because no org root exists yet.
Workspace setup
Start from the parent directory that will contain one or more
<instance>/<organization> workspaces:
mkdir elementum-workspace
cd elementum-workspace
npm init -y
npm install @elementumai/edk
elementum auth login --profile <profile>
elementum --profile=<profile> pull org --data-only
cd <instance>/<organization>
elementum --profile=<profile> initpull org --data-only bootstraps and refreshes org reference identities. Plain
pull org additionally writes editable organization.ts, adopts supported
org-owned resources, and keeps the generated org.ts catalog separate. It
creates the package/workspace scaffolding, org.ts, the @catalog path alias,
and the Terraform input/output skeleton:
<workspace>/
package.json
<instance>/
<organization>/
org.ts
organization.ts # editable org resources after plain `pull org`
tsconfig.json
backend.tf # optional locking backend; user-owned and committed
terraform.tfstate # committed local state when backend.tf is absent
apps/
elements/
generated/
catalog.ts
.tf/ # generated and ignored
in/ # bridge export input
out/ # SDK build outputelementum init is an idempotent installation/workspace convergence command.
It verifies the managed compatibility train, auth, SDK namespace/version,
workspace layout, and provider identity. It never logs in or silently migrates
Terraform state.
Do not hand-edit org.ts, generated/, or .tf/. Refresh those through their
owning commands.
Recommended flow
Pull first when editing an object that already exists:
cd <workspace>/<instance>/<organization>
elementum --profile=<profile> pull org --data-only
elementum --profile=<profile> pull app <namespace>
# or: elementum --profile=<profile> pull element <namespace>
# edit apps/<ref>/... or elements/<ref>/...
npx tsc --noEmit
elementum build
elementum --profile=<profile> plan
elementum --profile=<profile> applypull app and pull element reconstruct editable TypeScript, regenerate the
catalog, then automatically adopt the exported resources into the configured
Terraform state. Adoption is gated to exactly the expected imports, zero
create/update/destroy operations, and a clean follow-up plan. Use --no-adopt
only when you deliberately want source without state adoption.
Pull commands reserve stdout for one JSON summary. Diagnostics and stream kinds that do not yet have an extractor are reported rather than silently discarded.
CLI commands
The TypeScript-owned command surface is:
elementum init [--check] [--json]
elementum update [--check | --dry-run] [--channel stable|preview] [--version <semver>]
elementum migrate backend [orgRoot] [--check | --dry-run | --auto-approve] [--reconfigure]
elementum migrate provider-source [orgRoot] [--check | --dry-run | --auto-approve]
elementum pull org [--root <orgRoot>] [--no-adopt]
elementum pull org --data-only [--root <orgRoot>]
elementum pull app <namespace> [--root <orgRoot>] [--no-adopt]
elementum pull element <namespace> [--root <orgRoot>] [--no-adopt]
elementum clone app <namespace> --as <newName> [--namespace <newns>] [--root <orgRoot>]
elementum build [orgRoot]
elementum plan [orgRoot-or-nested-target] [bridge args...]
elementum apply [orgRoot-or-nested-target] [bridge args...]
elementum list apps|elements|users|groups|categories|cloud-links
elementum list ai-provider-connectors|automations|tables
elementum new app --name "Support Tickets" --namespace support --category "Operations"
elementum new element --name "Locations" --namespace locations --category "Operations"
elementum new agent --app <appRef> --name "Triage Bot"
elementum new skill --app <appRef> --name "Lookup Ticket"
elementum new automation --app <appRef> --name "Escalate Ticket"
elementum generate [path] [--root <orgRoot>]
elementum canvas <orgRoot-or-file> [--port <port>] [--no-open]
elementum playbooks list|install|status|path|printauth and other bridge-owned commands route through the managed bridge. Run
elementum --help for the combined local and bridge command surface.
build loads app, element, and supported child authoring files and replaces its
marker-owned files under .tf/out/; it never calls the platform. plan and
apply run from .tf/out/, initialize OpenTofu there when necessary, and
forward extra arguments:
elementum plan <instance>/<organization>/apps/myApp/myApp.ts
elementum --profile=<profile> apply <instance>/<organization> -auto-approveThe optional path may be the org root or anything nested beneath it. Run from
any directory inside the marked org workspace to omit it. The CLI always runs
the bridge/OpenTofu pass-through from that root's .tf/out/; it does not scan
or deploy an individual target file.
new app and new element create offline org-root scaffolds — the initial
authoring file for a brand-new app or element with no existing platform
counterpart — and immediately register the new entity in the org's generated
catalog. new agent, new skill, and new automation create offline
app-owned scaffolds without overwriting an existing ref (automations are
app-owned only, for now). Here, skill means an Elementum
agentic skill attached to an agent.
generate resyncs generated/catalog.ts files with whatever authoring
source is currently on disk — the fix for hand-editing an existing file
(e.g. adding an automation's onDemand trigger parameter or an .outputs()
key) rather than running one of the new/pull/clone commands that
regenerate the catalog as a side effect of writing a brand-new file. Pass a
path to scope it to one app/element; omit it to resync the whole workspace.
It always refreshes the org-level barrel too.
Coding-agent guidance is packaged separately as
playbooks (Agent Skills-standard SKILL.md directories):
elementum playbooks list
elementum playbooks install --yesThe installed slash commands are /elementum, /elementum-uat,
/elementum-debug, /elementum-deployments, and /elementum-design.
elementum skills ... remains the platform command family for agentic-skill
operations.
Canvas
Canvas is an offline, live-reloading view of authored source:
elementum canvas <instance>/<organization>
elementum canvas apps/myApp/automations/someAutomation.tsA directory target opens the workspace explorer at http://localhost:3456,
covering pulled org identities, apps, fields, status/stages, agents and their
tools, and supported automations. A file target renders the first export with
toUI() (or .build().toUI()). Use --port <port> to choose another port or
--no-open to suppress browser launch.
Package imports
Import public package subpaths, not repository-relative modules:
import { calc, app } from "@elementumai/edk/app";
import { element } from "@elementumai/edk/elements";
import {
actions,
automation,
onDemand,
} from "@elementumai/edk/automations";
import {
agentCreateRecord,
agentRunAutomation,
agentSearchRecords,
agent,
} from "@elementumai/edk/agents";
import { skill } from "@elementumai/edk/agents/skills";
import { linkedSearchTable, searchTable, searchTableDuration } from "@elementumai/edk/searchTables";
import { approvalProcess } from "@elementumai/edk/approvalProcesses";
import { table } from "@elementumai/edk/tables";
import { eq } from "@elementumai/edk/core/filters";
import catalog from "@catalog";The package exports and typesVersions map are kept in lockstep. Generated
source uses the same public subpaths.
Authoring model
EDK uses names and typed tokens in source, with the configured OpenTofu state backend providing platform identity:
pull org --data-onlywrites org reference bags toorg.ts, including cloud links, phone providers, categories, connectors, and groups. Plainpull orgalso writes editable categories, groups, CloudLinks, email aliases, and Twilio providers toorganization.ts.pull apporpull elementwrites editable source and adopts existing resources into the required locking remote backend.generated/catalog.tsis a pure barrel derived from the authored file tree. Import app/entity tokens from@catalog; import org tokens fromorg.ts.buildconverts TypeScript to the one supported HCL dialect in.tf/out/.- OpenTofu resolves references and computes create/update/replace/destroy
behavior during
planandapply.
There are no UUIDs or minted handles in authored source. The TypeScript bag key
is the Terraform label in camelCase through a lossless label conversion. For
example, a field's key in fields: {} is its identity; its display name can
change.
Organization credentials use secret("variableName"); the build emits a
sensitive Terraform variable and reads its value from TF_VAR_variableName.
Pull never serializes write-only credentials. It leaves redactedSecret(...)
or unavailableValue(...) markers and reports those addresses as skipped until
the missing input is supplied, while still adopting fully recoverable org
resources.
Configured builders such as agent and automation return
{ ref(refName), def(refName) }. Workspace discovery supplies the ref name from
the file path. There is no .deploy() method.
Automation example
The Terraform path round-trips typed triggers and actions. Public authoring uses direct fluent action methods; provider task resource names remain an internal wire concern.
import { automation, onDemand } from "@elementumai/edk/automations";
import { template } from "@elementumai/edk/core";
import catalog from "@catalog";
export default automation({
name: "Greet New Student",
app: catalog.students,
revision: "1.0.0",
actions: actions()
.trigger(onDemand({ STUDENT_NAME: "text" }))
.setVariable("GREETING", {
value: (ctx) => template("Welcome, ", ctx.trigger.STUDENT_NAME),
})
.executeScript("FORMAT_GREETING", {
inputs: (ctx) => ({ greeting: ctx.variables.GREETING }),
code: (input) => ({
message: `${input.parameters.greeting}!`,
}),
})
.outputs((ctx) => ({
message: ctx.actions.FORMAT_GREETING.result.message,
})),
});The file path supplies the automation ref; app is the strongly typed owning app ref.
revision is required SemVer and is the explicit publish gate. Editing
actions or triggers updates the draft; bump the revision only when that draft
should be published.
Agent example
Agents live under apps/<appRef>/agents/<agentRef>.ts. Their connector comes
from the org catalog, while app and field targets come from @catalog:
import { agentSearchRecords, agent } from "@elementumai/edk/agents";
import catalog from "@catalog";
import { org } from "../../../org.js";
export default agent({
name: "Support Agent",
app: catalog.support,
connector: org.connectors.claudeSonnet,
description: "Finds support records.",
instructions: "Search records and summarize the next action.",
tools: [
agentSearchRecords({
name: "find_records",
description: "Searches support records.",
app: catalog.support,
queryDescription: "Describe the record to find.",
fields: [
{
field: catalog.support.fields.status,
description: "Current status",
},
],
}),
],
});The Terraform path currently round-trips create-record, record-search, and run-automation agent tools. Unsupported tools and runtime-only knobs are reported as build diagnostics instead of being emitted as partial resources. Authored tool names use 1–64 letters, digits, or underscores, with no doubled underscores.
Plan and apply
Always build before planning:
cd <workspace>/<instance>/<organization>
elementum build
elementum plan
elementum applyplan and apply are pass-throughs to the managed bridge with inherited
stdio. With no explicit profile, the CLI selects the unique saved profile
matching the org root's <instance>/<organization> path. Review destroy and
replacement actions just as you would in any Terraform plan.
Identity and state recovery
Terraform state is the durable binding between authored labels and live
platform objects. For backwards compatibility, a workspace without
<orgRoot>/backend.tf continues to use the committed root
terraform.tfstate through a generated local backend. This preserves existing
single-author workspaces, but it provides no state locking; avoid concurrent
plan/apply operations in that mode.
Shared workspaces and CI should commit one user-owned, non-secret
<orgRoot>/backend.tf containing a complete
terraform { backend "TYPE" { ... } } declaration with verified locking.
elementum build projects either that declaration or the compatibility local
backend into .tf/out/backend.tf. Never commit .terraform/, .tf/, backend
credential files, or saved plan files. After migrating to remote state, remove
and stop committing the root terraform.tfstate.
For example, an S3 backend with native locking can be declared as:
terraform {
backend "s3" {
bucket = "elementum-team-state"
key = "prod/acme/terraform.tfstate"
region = "us-east-1"
dynamodb_table = "elementum-state-locks"
encrypt = true
}
}The backend body remains native OpenTofu HCL, but EDK validates the locking
contract. Native-locking backends azurerm, cos, gcs, kubernetes, pg,
and remote are accepted. consul must omit lock (the default is true) or
set lock = true. s3 must set use_lockfile = true or dynamodb_table;
http must set both
lock_address and unlock_address; oss must set both
tablestore_endpoint and tablestore_table. Optional-backend lock settings
must be literal so EDK can verify them before state access. An explicitly
authored local backend, unlocked/unknown backends, and configurations EDK
cannot verify fail closed; the generated local compatibility backend is the
only local exception.
The storage and locking infrastructure and access policies must already exist;
EDK does not provision them.
Backend authentication must use that backend's standard environment or
workload-identity chain: for example AWS/GCP/Azure OIDC, an IAM role, or
environment variables supplied by CI. Do not put access keys, passwords,
tokens, secret-manager data sources, or credential paths in backend.tf.
Backend initialization happens before providers and data sources, so it cannot
read a Terraform-managed secret. EDK does not accept -backend-config secrets
and never writes backend credentials into generated HCL.
OpenTofu owns locking. EDK does not implement a weaker parallel lock or disable
native locking: plan, apply, adoption imports, rename state mv, and
state-repair operations use the declared backend. Standard options such as
-lock-timeout=5m pass through:
elementum --profile=<profile> plan <orgRoot> -lock-timeout=5m
elementum --profile=<profile> apply <orgRoot> -auto-approve -lock-timeout=5mBackend initialization is lazy and noninteractive. A per-working-directory fingerprint prevents ordinary plan/apply/state commands from silently re-binding a changed backend; use the guarded migration command instead.
Migrate committed local state to a remote backend
Create the backend infrastructure first, then add and review backend.tf.
The migration uses native tofu init -migrate-state, takes an ignored recovery
snapshot, refuses to overwrite unrelated destination state, verifies
lineage/resources, removes the obsolete root copy, and requires a final
zero-change plan:
elementum migrate backend <orgRoot> --check
elementum migrate backend <orgRoot> --dry-run
elementum migrate backend <orgRoot> --auto-approve
elementum --profile=<profile> plan <orgRoot>For a remote-to-remote change, edit backend.tf and run the same migration.
Use --reconfigure only to bind an already-populated destination without
copying the currently bound state. It is rejected while local committed state
still exists.
Recovery snapshots and exact rollback commands are printed on failure and kept
under .tf/migration-backups/backend/. The command updates .gitignore but
does not rewrite Git history or mutate the index. After a successful
local-to-remote migration, commit the root-state deletion with backend.tf and
.gitignore; review the diff before pushing:
git add backend.tf .gitignore
git add -u terraform.tfstate
git commitIf state is lost, do not apply the built configuration: OpenTofu would see the live objects as new. Restore/push the recovery snapshot or re-adopt each stream with:
elementum --profile=<profile> pull org --data-only
elementum --profile=<profile> pull app <namespace>
elementum --profile=<profile> pull element <namespace>After an update reports an old provider identity, inspect and explicitly migrate the configured state:
elementum migrate provider-source <orgRoot> --check
elementum migrate provider-source <orgRoot> --auto-approve
elementum --profile=<profile> plan <orgRoot>The migration reads the configured local or remote state, creates an ignored
recovery snapshot, runs backend-bound OpenTofu state replace-provider,
refreshes generated provider configuration, and never applies infrastructure.
CI/CD
Use a serialized protected deployment job and let the backend lock arbitrate all writers. Supply Elementum auth through the bridge's CI profile setup and backend auth through workload identity/environment variables. A current GitHub Actions job (the public signed toolchain is macOS-only today) looks like:
jobs:
apply:
runs-on: macos-14
concurrency:
group: elementum-prod
cancel-in-progress: false
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: elementum build ./us/acme
- run: elementum --profile=ci plan ./us/acme -lock-timeout=5m
- run: elementum --profile=ci apply ./us/acme -auto-approve -lock-timeout=5mThe command sequence and environment contract are identical on a noninteractive Linux runner when your organization supplies the Linux toolchain artifact; the public installer does not yet distribute Linux binaries.
Create and update semantics
Terraform owns lifecycle decisions; EDK has no second deployment state machine:
- Configured resource absent from state: create.
- Resource present in config and state with changed attributes: update in place or replace, according to provider schema.
- Resource present in state but absent from built config: destroy.
- Existing platform resource mapped into state by pull: adopt.
Pull adoption's zero-write gates prevent an existing resource from being mistaken for a create. Build diagnostics are part of the safety contract: unsupported stream kinds are never silently adopted or emitted. Renames that change an exported Terraform label are guarded during adoption and rebound with state moves when the existing platform identity can be proven.
Development
The repository builds three distinct output trees:
dist-sdk/—tsc -p tsconfig.sdk.jsonoutput, the sole source tree packed into the@elementumai/edkSDK package;dist-cli/— Node-compatibletsc -p tsconfig.cli.jsonoutput used by local development, tests, integration tooling, and the binary build script; it is not part of the SDK package or the final release payload;dist-bin/— standaloneelementumexecutables produced fromdist-cliby the locked local Bun dependency; these are the CLI release inputs distributed separately from npm.
The repository's principal source roots are siblings:
src/ # TypeScript SDK and public CLI
cmd/elementum/ # temporary private Go bridge module
provider/ # OpenTofu provider module
distribution/ # registry and installer packageContributor setup, change-specific tests, Go/provider checks, and pull-request verification are documented in CONTRIBUTING.md. The shortest local CLI path is:
npm run local:setup
npm run elementum -- --help
npm run test:cliSee binary validation for release and compatibility-train checks.
Contributor integration harness
The Preston High harness creates, mutates, renames, and deletes live resources. Use only a dedicated test organization and profile. The complete setup, required org identities, golden-workspace flow, command flags, and failure semantics live in the Golden integration test.
