Quickstart
This guide takes you from an empty directory to a running Greengrass v2 component talking to a
local MQTT broker. You will scaffold a component with the edgecommons CLI, look at what it
generated, wire up the bootstrap code, build it, and run it on the HOST platform.
Pick your language with the tabs in each code block — the tabs stay in sync across the page.
-
Install the prerequisites
Section titled “Install the prerequisites”The scaffolding CLI is a single static binary. At a minimum you need git, a
rustuptoolchain (to build the CLI), Docker (for the local MQTT broker), and the toolchain for the language you are targeting:- Java — JDK 25 and Maven. (The library targets Java 25, so the JDK must be 25 even though the generated component compiles to an older bytecode level.)
- Python — Python 3.9+.
- Rust — a
rustuptoolchain. The generated component crate is edition 2021 / MSRV 1.85. - TypeScript — Node 18+ and npm.
See the Installation guide for full setup, including how to make the
edgecommonslibrary resolvable for each language. Once the toolchain is in place, install the CLI and confirm your environment:
# From the monorepo root — installs the `edgecommons` command.cargo install --path cli/crates/ec-cliedgecommons doctor --platforms HOST # checks only what a HOST component needs# From the monorepo root — installs the `edgecommons` command.cargo install --path cli/crates/ec-cliedgecommons doctor --platforms HOST # checks only what a HOST component needsedgecommons doctor prints [ok] <name> -> <path> (with the tool’s version) or
[missing] <name> for each tool, and exits non-zero when a required one is missing. --platforms
and -l/--language narrow it to the tools your workflow actually uses.
-
Scaffold a component
Section titled “Scaffold a component”edgecommons component newgenerates a component from a template carried inside the binary, so scaffolding works offline. The output directory is<path>/<kebab-name>, wherekebab-nameis derived from the last dot-segment of the name you pass by a case-boundary- and acronym-aware kebab conversion (com.example.MyComponentproduces amy-component/directory;--bin-nameand--diroverride the derived name and the directory outright — see the command reference for the full flag set). The Greengrass component name itself stays the PascalCase reverse-DNS form you passed — only the crate/bin/artifact tokens and the directory move to kebab.Run it with no flags on a terminal and it asks for the language and the name:
edgecommons component newedgecommons component newOr pass the flags directly. Use -l to choose the language (JAVA, PYTHON, RUST, or
TYPESCRIPT):
# Choose -l to match the language tab you are following.edgecommons component new -n com.example.MyComponent -l JAVA# -l PYTHON | -l RUST | -l TYPESCRIPTedgecommons component new -n com.example.MyComponent -l JAVA# -l PYTHON | -l RUST | -l TYPESCRIPTOn success the CLI prints Done. Component generated at: <dir>. By default the component targets
all platforms (--platforms GREENGRASS,HOST,KUBERNETES) and uses a local library dependency
(--dep-source local). --platforms selects the artifact packs: the Kubernetes Dockerfile and
k8s/ manifests are emitted only when KUBERNETES is among them, and the Greengrass recipe.yaml
and gdk-config.json only when GREENGRASS is.
Every scaffold also ships a config.schema.json describing the component’s own configuration — the
object at component.global. It is what edgecommons component validate checks your config
against, so a typo’d key is caught before it reaches a device.
-
Tour the generated project
Section titled “Tour the generated project”The layout differs slightly per language, but every component carries your business-logic file, a build manifest, the Greengrass
recipe.yaml+gdk-config.json, and sample configs undertest-configs/.
my-component/├─ src/main/java/.../MyComponent.java # your business logic — class stays PascalCase├─ pom.xml # Maven build (shaded JAR), artifactId = my-component├─ recipe.yaml gdk-config.json # Greengrass recipe + GDK config└─ test-configs/MyComponent.json # sample component configmy-component/├─ main.py # entry point — builds EdgeCommons, starts the app├─ app/MyComponent.py # your business logic├─ requirements.txt # depends on edgecommons├─ recipe.yaml gdk-config.json└─ test-configs/config_1.json config_2.jsonmy-component/├─ src/main.rs src/app.rs # entry point + your business logic├─ Cargo.toml build.sh # [package] name = "my-component"├─ recipe.yaml gdk-config.json└─ test-configs/config.json standalone-messaging.jsonmy-component/├─ src/main.ts src/app.ts # entry point + your business logic├─ package.json tsconfig.json build.sh├─ recipe.yaml gdk-config.json└─ test-configs/config.json standalone-messaging.json (+ .npmrc if --dep-source registry)Every layout above also carries AGENTS.md, CLAUDE.md, DESIGN.md, a Diátaxis docs/ set, and
.github/workflows/ — trimmed from these listings for brevity. The top-level directory is always
the kebab name; only the Java class file and the Greengrass component identity keep the PascalCase
form.
The sample config under test-configs/ already declares the component’s
UNS identity — a hierarchy
(["site", "device"]) plus an identity value for the site — and the default heartbeat, so the
generated component announces itself on the unified namespace out of the box. The app skeleton
mints any topic it publishes through gg.uns() rather than hand-building topic strings.
-
Understand the bootstrap code
Section titled “Understand the bootstrap code”The entry point builds one
EdgeCommonsruntime from the standard CLI args, then reads each subsystem off it via typed accessors. The accessor names follow each language’s conventions (getMessaging()/get_messaging()/messaging()), but the shape is the same everywhere.
import com.mbreissi.edgecommons.EdgeCommons;import com.mbreissi.edgecommons.EdgeCommonsBuilder;
public class MyComponent { public static void main(String[] args) { // Build the runtime from the standard CLI args (use the FULL component name). EdgeCommons gg = EdgeCommonsBuilder.create("com.example.MyComponent") .withArgs(args) .build();
var config = gg.getConfigManager(); var messaging = gg.getMessaging(); var metrics = gg.getMetrics();
// ... your business logic ... // Do NOT register your own shutdown hook: the library wires SIGTERM/SIGINT to a // graceful, idempotent shutdown() for you. }}The shipped Java template uses the deprecated direct constructor
(new EdgeCommons(name, args)); the builder shown here is the canonical convention and is what
the worked example uses.
import sysfrom edgecommons import EdgeCommonsBuilder
def main(): gg = (EdgeCommonsBuilder.create("com.example.MyComponent") .with_args(sys.argv[1:]) .build())
config = gg.get_config_manager() messaging = gg.get_messaging() # returns the MessagingClient class (its ops are static) metrics = gg.get_metrics() try: ... # your business logic finally: gg.shutdown() # or: with EdgeCommonsBuilder.create(...).build() as gg:
if __name__ == "__main__": main()The library installs SIGTERM and SIGINT handlers (main thread only) that call shutdown() for
you; calling shutdown() yourself is safe and idempotent.
use edgecommons::prelude::*;
const COMPONENT_NAME: &str = "com.example.MyComponent";
#[tokio::main]async fn main() -> anyhow::Result<()> { let gg = EdgeCommonsBuilder::new(COMPONENT_NAME) .args(std::env::args_os()) // argv INCLUDES the program name .build() .await?;
let config = gg.config(); tracing::info!(thing = %config.thing_name, "starting");
if let Ok(messaging) = gg.messaging() { // messaging() returns Result, not Option let _ = messaging; // ... publish / subscribe ... }
Ok(()) // dropping `gg` releases all resources (RAII) — there is no close()}The signal watcher only flips readiness when a stop signal arrives; teardown happens when
gg is dropped at the end of main.
import { EdgeCommonsBuilder, logger } from "@edgecommons/edgecommons";
const COMPONENT_NAME = "com.example.MyComponent";
async function main(): Promise<void> { const gg = await new EdgeCommonsBuilder(COMPONENT_NAME) .args(process.argv.slice(2)) // argv WITHOUT the node/script prefix .build();
const config = gg.config(); const messaging = gg.messaging(); // throws if no transport is wired const metrics = gg.metrics(); logger.info(`starting: thing=${config.thingName}`);
// ... your business logic ... // The library wires SIGTERM + SIGINT to gg.close() for you.}
main().catch((err) => { console.error("fatal:", err); process.exit(1); });The npm package is @edgecommons/edgecommons (not a bare edgecommons). close() is idempotent and
the library calls it on a stop signal.
-
Build the component
Section titled “Build the component”Build with your language’s standard toolchain. These commands are the same on Linux and Windows (forward-slash paths work in PowerShell too).
mvn clean package # produces target/my-component-1.0.0.jar (shaded, self-contained)pip install -r requirements.txt # pulls in edgecommons; no compile stepcargo build # standalone build — works on any OSnpm install # for a local dep, add --install-links to copy rather than symlinknpm run build # tsc -> dist/-
Start a local MQTT broker
Section titled “Start a local MQTT broker”The HOST platform with the MQTT transport connects to a local broker (and, optionally, to AWS IoT Core). Bring up the EMQX broker that ships with the repo — it exposes
1883(plaintext) and8883(mutual TLS):
docker compose -f test-infra/compose.yaml up -d# Or a throwaway broker anywhere: docker run -d -p 1883:1883 emqx/emqx:latestdocker compose -f test-infra/compose.yaml up -d# Or a throwaway broker anywhere: docker run -d -p 1883:1883 emqx/emqx:latestThe MQTT transport needs a small messaging-config JSON (the --transport MQTT <path> payload):
messaging.local is required and messaging.northbound is optional. Each running process needs a
unique clientId. The Rust and TypeScript templates already ship one at
test-configs/standalone-messaging.json; for Java and Python, create a
standalone-messaging.json in the component directory:
{ "messaging": { "local": { "host": "localhost", "port": 1883, "clientId": "my-component-local" } }}-
Run it
Section titled “Run it”Start the component on the HOST platform, pointing
--transport MQTTat the messaging config and-c FILEat a component config, with a Thing name via-t. Run from inside the generated component directory.
java -jar target/my-component-1.0.0.jar \ --platform HOST --transport MQTT ./standalone-messaging.json \ -c FILE test-configs/MyComponent.json -t my-thingpython3 main.py \ --platform HOST --transport MQTT ./standalone-messaging.json \ -c FILE test-configs/config_2.json -t my-thingcargo run -- \ --platform HOST --transport MQTT ./test-configs/standalone-messaging.json \ -c FILE ./test-configs/config.json -t my-thingnode dist/main.js \ --platform HOST --transport MQTT ./test-configs/standalone-messaging.json \ -c FILE ./test-configs/config.json -t my-thingTo confirm the component is alive, subscribe to ecv1/+/+/state (add ecv1/+/+/+/state for instance-scoped publishers) on the broker (for example
with MQTTX) and watch the periodic state keepalives arrive on
ecv1/<thing>/<component>/state — every scaffolded component publishes them by default,
and mints its own topics through the Unified Namespace builder
(gg.uns()).