What Will You Learn in This Chapter?
In this chapter, you'll learn how to develop AOS (Actor Operating System), the first implementation of the AO protocol.
AOS is a lightweight Lua-based actor runtime that combines Actor Model × Persistent Storage × Distributed Messaging into a new execution environment.
You'll gain hands-on experience by writing actual code, going through the complete process from process creation → logic registration → message sending and receiving step by step.
What You'll Master in This Chapter:
- Overview of AOS and usage of the
aoconnect
development library - Connection methods to MU/SU/CU and process creation flow
- Defining and registering handlers in Lua
- Message sending, receiving, and simulation with dryrun
- Building practical processes like Key-Value stores
- Best practices for secure and reusable AOS development
What is AOS
AOS (Actor Operating System) is a Lua-based distributed actor execution environment developed as the first implementation of the AO protocol.
It faithfully implements the actor model and messaging structures defined by AO, designed to enable anyone to easily build and deploy actor-based applications.
AOS has the following characteristics:
- Lightweight Lua Runtime
- Lua is an extremely lightweight scripting language also used for embedded applications, making it easy to compile to Wasm.
- Full Actor Model Support
- Each process has independent state and an inbox, communicating with each other through asynchronous messages.
- Arweave-based Persistence
- Process state and message history are permanently stored on Arweave and can be restored at any time.
- Dynamic Logic Registration via Eval
- Even after deployment, you can flexibly extend or modify logic by sending Lua scripts to processes.
- Complete Automation via Scripts
- Process creation, manipulation, and testing can all be controlled from JavaScript through the
aoconnect
library.
AOS is the ideal entry point for understanding AO's fundamental concepts and quickly building your own distributed actor applications.
In this chapter, we'll learn the complete flow of process development using AOS with actual code examples.
AOS Setup
AOS is one of the first implementations of AO and uses the Lua language.
To use AOS, first install the aoconnect
library.
npm i @permaweb/aoconnect
Connecting to Units
To use AOS in a local environment, connect by specifying the URLs for each unit (MU/SU/CU).
const { createDataItemSigner, connect } = require("@permaweb/aoconnect");
const { result, message, spawn, dryrun } = connect({
MU_URL: "http://localhost:4002",
CU_URL: "http://localhost:4004",
GATEWAY_URL: "http://localhost:4000",
});
For mainnet usage, you can connect simply as follows:
const { result, message, spawn, dryrun } = require("@permaweb/aoconnect");
Spawning an AOS Process
To spawn an AOS process, you need the AOS module ID and scheduler wallet address.
Here's the basic code for spawning a process:
const wait = (ms) => new Promise((res) => setTimeout(() => res(), ms));
const pid = await spawn({
module: AOS_MODULE_TXID, // The module transaction ID you noted earlier
scheduler: SCHEDULER_WALLET, // Scheduler wallet address
signer: createDataItemSigner(jwk),
});
await wait(1000);
console.log(`Generated Process ID: ${pid}`);
Once an AO process is spawned, it exists permanently on Arweave and can receive and process messages.
Lua Handler Basics
In AOS, you write process business logic using the Lua language.
The concept of "handlers" is particularly important.
Handlers are functions that execute when a message matching specific conditions is received.
AOS provides the following global variables and modules:
- Inbox: Table storing unprocessed messages
- Send: Function for sending messages
- Spawn: Function for spawning new processes
- Handlers: Table for managing handlers
- ao: Module providing process information and message sending functionality
- json: Module for JSON encoding/decoding
Here's the basic structure of a handler:
Handlers.add(
"handler-name", -- Handler identifier
function(msg) -- Matching function: determines if a message should be processed by this handler
return msg.Action == "specific-action" -- Returns true if condition matches
end,
function(msg) -- Handler function: processes the message
-- Logic to process the message
ao.send({Target = msg.From, Data = "response message"})
end
)
Handlers.utils
provides commonly used matching functions:
-- Match messages with matching tags
Handlers.utils.hasMatchingTag("Action", "Get")
-- Match messages with matching data
Handlers.utils.hasMatchingData("ping")
-- Utility for easily sending responses
Handlers.utils.reply("pong")
Creating and Registering Handlers
Let's implement a simple Key-Value store here.
Save the following Lua code to a kv-store.lua
file:
local ao = require('ao')
Store = Store or {}
Handlers.add(
"Get",
Handlers.utils.hasMatchingTag("Action", "Get"),
function (msg)
assert(type(msg.Key) == 'string', 'Key is required!')
ao.send({ Target = msg.From, Tags = { Value = Store[msg.Key] }})
end
)
Handlers.add(
"Set",
Handlers.utils.hasMatchingTag("Action", "Set"),
function (msg)
assert(type(msg.Key) == 'string', 'Key is required!')
assert(type(msg.Value) == 'string', 'Value is required!')
Store[msg.Key] = msg.Value
Handlers.utils.reply("Value stored!")(msg)
end
)
To add this code to an existing process, use the Eval
action as follows:
const { readFileSync } = require("fs");
const { resolve } = require("path");
const lua = readFileSync(resolve(__dirname, "kv-store.lua"), "utf8");
const mid = await message({
process: pid,
signer: createDataItemSigner(jwk),
tags: [{ name: "Action", value: "Eval" }],
data: lua,
});
const res = await result({ process: pid, message: mid });
console.log(res);
This adds Key-Value store functionality to the process.
Sending and Receiving Messages
To send messages to a process, use the message
function:
const mid1 = await message({
process: pid,
signer: createDataItemSigner(jwk),
tags: [
{ name: "Action", value: "Set" },
{ name: "Key", value: "test" },
{ name: "Value", value: "abc" },
],
});
const res1 = await result({ process: pid, message: mid1 });
console.log(res1.Messages[0]);
For read-only queries, using the dryrun
function is efficient:
const res2 = await dryrun({
process: pid,
signer: createDataItemSigner(jwk),
tags: [
{ name: "Action", value: "Get" },
{ name: "Key", value: "test" },
],
});
console.log(res2.Messages[0].Tags);
dryrun
doesn't actually save messages to Arweave and simulates using the current state, making it suitable for read operations.
Development Patterns and Best Practices
Here are some best practices for AOS development:
- State Management:
- When using global variables, don't forget initialization (
Store = Store or {}
) - Manage large data structures with proper nesting
- Error Handling:
- Use
assert
to validate inputs - Make error messages specific and clear
- Modularization:
- Group related functionality together
- Create reusable functions
- Debugging:
- Use the
ao.log
function to output debug information - Output complex data structures to logs using
json.encode
- Security:
- Always validate untrusted data
- Perform authentication checks for critical operations
-
Testing:
- Create dedicated processes for testing
- Use
dryrun
to verify changes before applying to production
Following these practices will help you develop safer and more efficient AOS applications.