What are AO Processes
AO processes are autonomous compute units that run on the Arweave network, enabling decentralized applications to execute complex logic permanently and trustlessly. Think of them as serverless functions that never go down and can maintain state across invocations.
Core Architecture
AO processes represent a paradigm shift from traditional smart contracts. Unlike Ethereum's synchronous execution model, AO processes operate asynchronously, communicating through message passing in a distributed network.
AO Process Architecture:
User/Application
↓ (Message)
AO Process ←→ Another Process
↓ ↑ (Messages)
State Update/Read
↓ ↑
Arweave Storage
This architecture demonstrates how AO processes communicate through asynchronous message passing while maintaining persistent state on Arweave. Each process operates independently while being able to interact with other processes in the network.
Key Components
Process Instance
- Unique process ID (43-character string)
- Lua-based execution environment
- Persistent state storage on Arweave
- Message inbox for receiving communications
Message System
- Asynchronous message passing
- Tagged messages for routing and filtering
- Cryptographic signatures for authentication
- Permanent message history on Arweave
State Management
- Deterministic state transitions
- Immutable state snapshots
- Conflict-free replicated data types (CRDTs)
- Rollback and replay capabilities
Process Lifecycle
1. Process Creation
Creating an AO process involves deploying Lua code to the network:
import { connect } from "@permaweb/aoconnect";
const ao = connect();
// Deploy a new process
const processId = await ao.spawn({
module: "MODULE_TX_ID", // Pre-compiled Lua module
scheduler: "SCHEDULER_ADDRESS", // Network scheduler
signer: createDataItemSigner(wallet), // Wallet signer
tags: [
{ name: "App-Name", value: "MyApp" },
{ name: "App-Version", value: "1.0.0" }
]
});
console.log("Process created:", processId);
2. Process Initialization
Once spawned, the process can be initialized with initial state:
// Send initialization message
await ao.message({
process: processId,
tags: [
{ name: "Action", value: "Initialize" }
],
data: JSON.stringify({
owner: "USER_ADDRESS",
name: "My Process",
version: "1.0.0"
}),
signer: createDataItemSigner(wallet)
});
3. Message Processing
Processes receive and handle messages according to their Lua handlers:
-- Example Lua handler in the process
Handlers.add(
"Initialize",
Handlers.utils.hasMatchingTag("Action", "Initialize"),
function(msg)
local data = json.decode(msg.Data)
State.owner = data.owner
State.name = data.name
State.initialized = true
ao.send({
Target = msg.From,
Data = "Process initialized successfully"
})
end
)
State Management Patterns
Deterministic State Updates
AO processes maintain deterministic state through ordered message processing:
-- State variables
Balance = Balance or 0
Transactions = Transactions or {}
-- Handler for balance updates
Handlers.add(
"UpdateBalance",
Handlers.utils.hasMatchingTag("Action", "UpdateBalance"),
function(msg)
local amount = tonumber(msg.Tags.Amount)
local operation = msg.Tags.Operation
if operation == "credit" then
Balance = Balance + amount
elseif operation == "debit" and Balance >= amount then
Balance = Balance - amount
else
ao.send({
Target = msg.From,
Data = "Insufficient balance"
})
return
end
-- Record transaction
table.insert(Transactions, {
id = msg.Id,
from = msg.From,
amount = amount,
operation = operation,
timestamp = msg.Timestamp,
balance = Balance
})
ao.send({
Target = msg.From,
Data = json.encode({
success = true,
balance = Balance,
transactionId = msg.Id
})
})
end
)
State Persistence
State is automatically persisted to Arweave through the process lifecycle:
-- State checkpoint handler
Handlers.add(
"SaveCheckpoint",
Handlers.utils.hasMatchingTag("Action", "SaveCheckpoint"),
function(msg)
local checkpoint = {
balance = Balance,
transactions = Transactions,
lastUpdate = msg.Timestamp,
version = "1.0.0"
}
-- State is automatically persisted
ao.send({
Target = msg.From,
Data = "Checkpoint saved",
Tags = {
{ name = "Checkpoint-Data", value = json.encode(checkpoint) }
}
})
end
)
Common Use Cases
1. Token Contracts
AO processes excel at implementing token logic:
-- Token contract implementation
Name = "MyToken"
Ticker = "MTK"
Denomination = 12
TotalSupply = 1000000 * 10^Denomination
Balances = { [Owner] = TotalSupply }
Handlers.add(
"Transfer",
Handlers.utils.hasMatchingTag("Action", "Transfer"),
function(msg)
local target = msg.Tags.Recipient
local quantity = tonumber(msg.Tags.Quantity)
if Balances[msg.From] and Balances[msg.From] >= quantity then
Balances[msg.From] = Balances[msg.From] - quantity
Balances[target] = (Balances[target] or 0) + quantity
-- Emit events
ao.send({ Target = msg.From, Data = "Transfer successful" })
ao.send({ Target = target, Data = "Tokens received" })
else
ao.send({ Target = msg.From, Data = "Insufficient balance" })
end
end
)
2. Decentralized Applications
Build complex dApps with multiple interacting processes:
-- DAO voting process
Proposals = Proposals or {}
Votes = Votes or {}
Handlers.add(
"CreateProposal",
Handlers.utils.hasMatchingTag("Action", "CreateProposal"),
function(msg)
local proposalId = msg.Id
Proposals[proposalId] = {
title = msg.Tags.Title,
description = msg.Data,
creator = msg.From,
created = msg.Timestamp,
status = "active",
votesFor = 0,
votesAgainst = 0
}
ao.send({
Target = msg.From,
Data = "Proposal created: " .. proposalId
})
end
)
3. Data Processing Pipelines
Chain processes together for complex workflows:
-- Data processing handler
Handlers.add(
"ProcessData",
Handlers.utils.hasMatchingTag("Action", "ProcessData"),
function(msg)
local data = json.decode(msg.Data)
-- Process the data
local processed = transformData(data)
-- Send to next process in pipeline
ao.send({
Target = msg.Tags.NextProcess,
Data = json.encode(processed),
Tags = {
{ name = "Action", value = "ReceiveProcessedData" },
{ name = "Source", value = ao.id }
}
})
end
)
Process Communication Patterns
Direct Messaging
Processes communicate directly through tagged messages:
// Send message to specific process
await ao.message({
process: targetProcessId,
tags: [
{ name: "Action", value: "GetBalance" },
{ name: "Account", value: userAddress }
],
signer: createDataItemSigner(wallet)
});
// Receive response
const result = await ao.result({
message: messageId,
process: targetProcessId
});
Pub/Sub Patterns
Implement publish-subscribe messaging:
-- Subscriber registration
Subscribers = Subscribers or {}
Handlers.add(
"Subscribe",
Handlers.utils.hasMatchingTag("Action", "Subscribe"),
function(msg)
local topic = msg.Tags.Topic
if not Subscribers[topic] then
Subscribers[topic] = {}
end
table.insert(Subscribers[topic], msg.From)
ao.send({
Target = msg.From,
Data = "Subscribed to " .. topic
})
end
)
-- Broadcast messages
Handlers.add(
"Publish",
Handlers.utils.hasMatchingTag("Action", "Publish"),
function(msg)
local topic = msg.Tags.Topic
if Subscribers[topic] then
for _, subscriber in ipairs(Subscribers[topic]) do
ao.send({
Target = subscriber,
Data = msg.Data,
Tags = {
{ name = "Topic", value = topic },
{ name = "Publisher", value = msg.From }
}
})
end
end
end
)
Development Best Practices
Error Handling
Implement robust error handling in your processes:
-- Comprehensive error handling
Handlers.add(
"SafeOperation",
Handlers.utils.hasMatchingTag("Action", "SafeOperation"),
function(msg)
local success, result = pcall(function()
-- Your operation logic here
local data = json.decode(msg.Data)
if not data.required_field then
error("Missing required field")
end
return processData(data)
end)
if success then
ao.send({
Target = msg.From,
Data = json.encode({ success = true, result = result })
})
else
ao.send({
Target = msg.From,
Data = json.encode({
success = false,
error = result,
timestamp = msg.Timestamp
})
})
end
end
)
Access Control
Implement proper authorization:
-- Role-based access control
Roles = {
[Owner] = "admin",
-- Add other role assignments
}
local function hasRole(address, requiredRole)
return Roles[address] == requiredRole
end
Handlers.add(
"AdminOnly",
Handlers.utils.hasMatchingTag("Action", "AdminOnly"),
function(msg)
if not hasRole(msg.From, "admin") then
ao.send({
Target = msg.From,
Data = "Access denied: Admin role required"
})
return
end
-- Admin logic here
end
)
Testing Strategies
Use AOS (AO Studio) for local development and testing:
# Install AOS for local testing
npm install -g https://get_ao.g8way.io
# Start AOS REPL
aos
# Load your process code
.load process.lua
# Test message handling
Send({ Action = "Test", Data = "test data" })
Performance Considerations
Message Optimization
Structure messages for efficient processing:
-- Batch operations for efficiency
Handlers.add(
"BatchTransfer",
Handlers.utils.hasMatchingTag("Action", "BatchTransfer"),
function(msg)
local transfers = json.decode(msg.Data)
local results = {}
for i, transfer in ipairs(transfers) do
local success = executeTransfer(transfer.to, transfer.amount)
table.insert(results, {
index = i,
success = success,
to = transfer.to,
amount = transfer.amount
})
end
ao.send({
Target = msg.From,
Data = json.encode(results)
})
end
)
State Management Optimization
Keep state lean and efficient:
-- Use efficient data structures
-- Instead of storing full transaction history:
-- Transactions = {} -- Can grow very large
-- Use rolling window or summary data:
RecentTransactions = {} -- Last 100 transactions
TransactionSummary = {
total_count = 0,
total_volume = 0,
last_updated = 0
}
Security Considerations
Input Validation
Always validate incoming data:
local function validateTransfer(msg)
local recipient = msg.Tags.Recipient
local quantity = tonumber(msg.Tags.Quantity)
if not recipient or recipient == "" then
return false, "Invalid recipient"
end
if not quantity or quantity <= 0 then
return false, "Invalid quantity"
end
if not Balances[msg.From] or Balances[msg.From] < quantity then
return false, "Insufficient balance"
end
return true, "Valid"
end
Reentrancy Protection
Protect against message replay attacks:
ProcessedMessages = ProcessedMessages or {}
local function isProcessed(messageId)
return ProcessedMessages[messageId] ~= nil
end
local function markProcessed(messageId)
ProcessedMessages[messageId] = true
end
Handlers.add(
"IdempotentHandler",
Handlers.utils.hasMatchingTag("Action", "IdempotentHandler"),
function(msg)
if isProcessed(msg.Id) then
return -- Already processed
end
-- Process the message
-- ... handler logic ...
markProcessed(msg.Id)
end
)
Monitoring and Debugging
Process Health Checks
Implement health monitoring:
Handlers.add(
"HealthCheck",
Handlers.utils.hasMatchingTag("Action", "HealthCheck"),
function(msg)
local health = {
status = "healthy",
uptime = msg.Timestamp - (StartTime or 0),
balance = Balance,
message_count = #ProcessedMessages,
last_activity = LastActivity or StartTime
}
ao.send({
Target = msg.From,
Data = json.encode(health)
})
end
)
Debugging Tools
Use logging for debugging:
-- Debug logging handler
local DEBUG_MODE = true
local function debugLog(message, data)
if DEBUG_MODE then
print("DEBUG [" .. os.date() .. "]: " .. message)
if data then
print("Data: " .. json.encode(data))
end
end
end
Handlers.add(
"DebugHandler",
function() return DEBUG_MODE end,
function(msg)
debugLog("Received message", {
action = msg.Tags.Action,
from = msg.From,
id = msg.Id
})
end
)
Next Steps
Now that you understand AO processes fundamentals:
- Learn process communication - Process Communication
- Master state management - State Management
- Explore HyperBEAM - HyperBEAM Introduction
- Build your first process - Builder's Journey
Resources
- AO Documentation: Official AO Docs
- AOS (AO Studio): Development Environment
- Code Examples: AO Cookbook Repository
- Community: AO Discord Channel