Embedding Lua in Node.js lets you run user-defined automation inside your process: fetch data, call an API, send mail, without a second runtime. wasmoon runs Lua 5.4 over WebAssembly. You bind JavaScript functions to Lua globals; scripts call those names like normal functions. Async JS returns Promises; from Lua you use :await() on the handle wasmoon gives you.
A typical pipeline (as in example-script.lua): pull Coinbase product (or candles), jsonStringify, OpenAI prompt, Gmail send. The bindings sketched below mirror that pattern; swap implementations for your own HTTP clients.
| Piece | Role |
|---|---|
| wasmoon | LuaFactory + createEngine() builds a LuaEngine. |
| Your code | engine.global.set('name', fn) exposes name to Lua. |
| Async | fn may return a Promise. In Lua use :await() on the returned value. |
| Run | await engine.doString(source) (or doFile) executes the script. |
Optional: engine.global.loadLibrary(LuaLibraries.Math) (and other wasmoon core libs) if you need standard library pieces beyond what you bind.
No framework - just create engine, register globals, run string. Wire fetchCoinbase, promptAi, mailSend to your real SDKs or fetch.
import { readFileSync } from "node:fs";
import { LuaFactory, LuaLibraries, type LuaEngine } from "wasmoon";
async function main() {
const factory = new LuaFactory();
const engine: LuaEngine = await factory.createEngine();
for (const lib of [
LuaLibraries.Base,
LuaLibraries.String,
LuaLibraries.Math,
]) {
engine.global.loadLibrary(lib);
}
engine.global.set("jsonStringify", (v: unknown) => JSON.stringify(v));
engine.global.set("coinbaseFetchProduct", (id: string) =>
fetchCoinbaseProduct(id),
);
engine.global.set("promptOpenAi", (body: string, instructions?: string) =>
promptOpenAi(body, instructions),
);
engine.global.set("sendGmail", (to: string, subject: string, text: string) =>
sendGmail(to, subject, text),
);
const lua = readFileSync("./example-script.lua", "utf8");
await engine.doString(lua);
}
// Implement these with your keys and HTTP clients; return Promises for async work.
async function fetchCoinbaseProduct(_id: string): Promise<unknown> {
/* ... */
return {};
}
async function promptOpenAi(
_body: string,
_instructions?: string,
): Promise<string> {
/* ... */
return "";
}
async function sendGmail(
_to: string,
_subject: string,
_text: string,
): Promise<void> {
/* ... */
}
main().catch(console.error);
JavaScript is the same without types: drop : LuaEngine, unknown, and Promise<...> annotations.
Same idea in fewer lines: one attachGlobals(engine) helper that registers only what that run needs.
example-script.lua - uses coinbaseFetchProduct; switch to coinbaseFetchCandles if you bind that name the same way.
local coinbaseProduct = coinbaseFetchProduct("BTC-USDC"):await()
local coinbaseProductStringified = jsonStringify(coinbaseProduct)
print(coinbaseProductStringified)
local openAiInstructions = "Analyze the current product pair on Coinbase"
openAiInstructions = openAiInstructions .. ". Provide a summary of the market conditions"
openAiInstructions = openAiInstructions .. ". Recommend buy/hold/sell"
local openAiPromptResponse = promptOpenAi(coinbaseProductStringified, openAiInstructions):await()
print(openAiPromptResponse)
sendGmail("all", "BTC-USDC Market Analysis", openAiPromptResponse)
promptOpenAi(body, instructions?) - First string is the main payload; second is optional guidance.sendGmail - "all" only works if your sendGmail implementation interprets it (e.g. expand to all saved recipients).Only register functions your scripts call. Names below match common samples; rename freely if your host uses shorter names.
| Lua global | Typical JS signature |
|---|---|
jsonStringify |
(value) => string |
jsonParse |
(json: string) => unknown (optional) |
coinbaseFetchProduct |
(productId: string) => Promise<unknown> |
coinbaseFetchCandles |
(productId, startIso, endIso, granularity) => Promise<unknown> |
promptOpenAi |
(prompt: string, instructions?: string) => Promise<string> |
sendGmail |
(to: string, subject: string, text: string) => Promise<void> |
Candle example (after you bind coinbaseFetchCandles):
local candles = coinbaseFetchCandles(
"BTC-USDC",
"2026-01-01T00:00:00Z",
"2026-01-02T00:00:00Z",
"ONE_HOUR"
):await()
local reply = promptOpenAi(jsonStringify(candles), "Summarize volatility and trend."):await()
sendGmail("all", "BTC-USDC hourly", reply)
Granularity must match whatever your Coinbase client expects (e.g. "ONE_HOUR", "ONE_DAY").
:await()Returning a Promise from JS:
local p = coinbaseFetchProduct("ETH-USDC")
local data = p:await()
Without :await(), data is not the resolved value.
engine.global.set("httpGet", (url: string) => fetch(url).then((r) => r.text()));
Prefer keeping secrets and I/O in JS; Lua stays orchestration.
wasmoon + global.set + doString is enough to host user Lua in a plain Node process. The example-script.lua pipeline is fetch - stringify - prompt - notify; extend by binding more functions, not by growing framework-specific wrappers.