Language
Docs

Documentation

Contributors: Dylan Shade
Last Updated:

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:

  1. Learn process communication - Process Communication
  2. Master state management - State Management
  3. Explore HyperBEAM - HyperBEAM Introduction
  4. Build your first process - Builder's Journey

Resources