DEV Community

KYOHEI ITO for Arweave Japan

Posted on • Edited on

Arweave AO Bootcamp 5/8 AOS Fundamental

What You'll Learn in This Chapter

In this chapter, you'll learn how to develop with AOS (Actor Operating System) — the first implementation of the AO protocol.
AOS is a lightweight, Lua-based actor runtime that combines the actor model, persistent storage, and decentralized messaging into a new execution paradigm.

Through hands-on examples, you’ll walk through the full lifecycle: spawning a process → registering logic → sending and receiving messages.

By the end of this chapter, you'll understand:

  • What AOS is and how to use the aoconnect library
  • How to connect to MU, SU, and CU in a local setup
  • How to write and register Lua handlers for message processing
  • How to send messages and simulate reads using dryrun
  • How to build a simple yet practical process like a key-value store
  • Best practices for writing secure, modular, and reusable AOS logic

What is AOS?

AOS (Actor Operating System) is the first implementation of the AO protocol — a Lua-based, distributed actor runtime.
It faithfully implements AO’s actor model and messaging architecture, enabling developers to easily build and deploy actor-driven applications on top of Arweave.

Key characteristics of AOS include:

  1. Lightweight Lua Runtime: Lua is a minimal and embeddable scripting language, ideal for compiling to WebAssembly and running in distributed environments.

  2. Full Actor Model Support: Each process maintains its own isolated state and inbox, communicating asynchronously via message passing.

  3. Arweave-Based Persistence: Process states and message histories are permanently stored on Arweave and can be restored at any time.

  4. Dynamic Logic with Eval: After deployment, logic can be extended or updated dynamically by sending Lua scripts to the process using the Eval action.

  5. Fully Scriptable via JavaScript: With the aoconnect library, every part of the development lifecycle — from spawning processes to sending messages — can be controlled programmatically.

AOS serves as an ideal entry point for learning AO’s architecture and rapidly prototyping your own decentralized actor applications.
In this chapter, you’ll explore the entire workflow of building with AOS through hands-on code examples.

AOS Fundamentals

Setting Up AOS

AOS is one of the first implementations of AO and uses the Lua programming language. To begin using AOS, install the aoconnect library:

npm i @permaweb/aoconnect
Enter fullscreen mode Exit fullscreen mode

Connecting to Units

To use AOS in a local environment, connect to each AO unit (MU, SU, CU) by specifying their URLs:

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",
});
Enter fullscreen mode Exit fullscreen mode

To connect to the mainnet, a simple import is sufficient:

const { result, message, spawn, dryrun } = require("@permaweb/aoconnect");
Enter fullscreen mode Exit fullscreen mode

Spawning an AOS Process

To spawn an AOS process, you need the transaction ID of the AOS module and the address of a scheduler wallet:

const wait = (ms) => new Promise((res) => setTimeout(() => res(), ms));

const pid = await spawn({
  module: AOS_MODULE_TXID, // Transaction ID of the module
  scheduler: SCHEDULER_WALLET, // Address of the scheduler wallet
  signer: createDataItemSigner(jwk),
});

await wait(1000);

console.log(`Spawned process ID: ${pid}`);
Enter fullscreen mode Exit fullscreen mode

Once the AO process is spawned, it exists permanently on Arweave and can start processing messages.

Basics of Lua Handlers

In AOS, Lua is used to define the business logic of each process. The core concept is the handler—a function triggered when a message matches specific criteria.

AOS provides the following global variables and modules:

  1. Inbox: A table holding unprocessed messages
  2. Send: A function to send messages
  3. Spawn: A function to create new processes
  4. Handlers: A table to register and manage handlers
  5. ao: A module providing process-related utilities
  6. json: Module for JSON encoding/decoding

Basic handler structure:

Handlers.add(
  "handler_name",
  function(msg)
    return msg.Action == "some_action"
  end,
  function(msg)
    ao.send({ Target = msg.From, Data = "response message" })
  end
)
Enter fullscreen mode Exit fullscreen mode

Common utility matchers from Handlers.utils:

Handlers.utils.hasMatchingTag("Action", "Get")
Handlers.utils.hasMatchingData("ping")
Handlers.utils.reply("pong")
Enter fullscreen mode Exit fullscreen mode

Creating and Registering a Handler

Here’s how to implement a simple key-value store. Save the following Lua code as kv-store.lua:

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
)
Enter fullscreen mode Exit fullscreen mode

To add this code to a running process, use the Eval action:

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);
Enter fullscreen mode Exit fullscreen mode

Sending and Receiving Messages

Use the message function to send data to a process:

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]);
Enter fullscreen mode Exit fullscreen mode

Use dryrun for read-only queries without writing to Arweave:

const res2 = await dryrun({
  process: pid,
  signer: createDataItemSigner(jwk),
  tags: [
    { name: "Action", value: "Get" },
    { name: "Key", value: "test" },
  ],
});
console.log(res2.Messages[0].Tags);
Enter fullscreen mode Exit fullscreen mode

Development Patterns and Best Practices

Here are some best practices for building with AOS:

State Management:

  • Always initialize global variables (e.g., Store = Store or {})
  • Structure large state data appropriately

Error Handling:

  • Use assert to validate inputs
  • Provide clear, descriptive error messages

Modularization:

  • Group related logic together
  • Create reusable functions when possible

Debugging:

  • Use ao.log for debugging output
  • Use json.encode to log complex structures

Security:

  • Always validate untrusted input
  • Implement authentication for critical actions

Testing:

  • Create separate processes for testing
  • Use dryrun to simulate changes before committing them

Following these practices will help you develop safer and more efficient AOS applications.

Top comments (0)