← Back to Documentation

Creating Custom Actions

Extend ARO with custom actions. Actions are the fundamental building blocks that implement ARO verbs like <Extract>, <Create>, <Return>, etc.

Understanding Actions

In ARO, every statement follows the Action-Result-Object pattern:

<Verb> the <result> from/to/with/for the <object>.

Each verb maps to an action implementation that:

  1. Receives structured information about the statement
  2. Executes business logic
  3. Returns a result to be bound to a variable

The ActionImplementation Protocol

public protocol ActionImplementation: Sendable {
    /// The semantic role of this action
    static var role: ActionRole { get }

    /// The verbs that trigger this action
    static var verbs: Set<String> { get }

    /// Valid prepositions for object clauses
    static var validPrepositions: Set<Preposition> { get }

    /// Required initializer
    init()

    /// Execute the action
    func execute(
        result: ResultDescriptor,
        object: ObjectDescriptor,
        context: ExecutionContext
    ) async throws -> any Sendable
}

Key Points

Action Roles

Actions are categorized by semantic role:

RoleDescriptionExample Verbs
requestRequest data from external sourcesExtract, Retrieve, Request
ownCreate or modify owned dataCreate, Compute, Transform, Validate
responseSend results or responsesReturn, Respond, Reply
exportExport or publish dataStore, Publish, Log, Send

Step-by-Step: Creating a Custom Action

Step 1: Define Your Action

import ARORuntime

public struct EmailAction: ActionImplementation {
    // 1. Define the semantic role
    public static let role: ActionRole = .export

    // 2. Define verbs that trigger this action
    public static let verbs: Set<String> = ["Email", "Mail"]

    // 3. Define valid prepositions
    public static let validPrepositions: Set<Preposition> = [.to, .with]

    // 4. Required initializer
    public init() {}

    // 5. Implement execute
    public func execute(
        result: ResultDescriptor,
        object: ObjectDescriptor,
        context: ExecutionContext
    ) async throws -> any Sendable {
        // Implementation here
    }
}

Step 2: Implement the Execute Method

public func execute(
    result: ResultDescriptor,
    object: ObjectDescriptor,
    context: ExecutionContext
) async throws -> any Sendable {
    // Get required service
    guard let emailService = context.service(EmailService.self) else {
        throw ActionError.serviceNotFound("EmailService")
    }

    // Get the email content
    let content: EmailContent = try context.require(result.identifier)

    // Get the recipient from the object
    let recipient: String
    switch object.sourceType {
    case .variable:
        recipient = try context.require(object.identifier)
    case .literal:
        recipient = object.identifier
    default:
        throw ActionError.invalidObjectSource(object.sourceType)
    }

    // Perform the action
    let sendResult = try await emailService.send(
        content: content,
        to: recipient
    )

    // Emit event for observability
    context.emit(EmailSentEvent(
        recipient: recipient,
        messageId: sendResult.messageId
    ))

    return sendResult
}

Step 3: Register Your Action

// In your application setup
ActionRegistry.shared.register(EmailAction.self)

Step 4: Use in ARO

(Send Welcome Email: User Onboarding) {
    <Create> the <email-content> with {
        subject: "Welcome to our platform!",
        body: "Thanks for signing up..."
    }.
    <Extract> the <user-email> from the <user: email>.
    <Email> the <email-content> to the <user-email>.
    <Return> an <OK: status> for the <email>.
}

Execution Context

The ExecutionContext provides access to runtime services:

public protocol ExecutionContext: AnyObject, Sendable {
    // Variable Management
    func resolve<T: Sendable>(_ name: String) -> T?
    func require<T: Sendable>(_ name: String) throws -> T
    func bind(_ name: String, value: any Sendable)
    func exists(_ name: String) -> Bool

    // Service Access
    func service<S>(_ type: S.Type) -> S?

    // Event Emission
    func emit(_ event: any RuntimeEvent)

    // Metadata
    var featureSetName: String { get }
    var executionId: String { get }
}

Best Practices

Single Responsibility

Each action should do one thing well:

// Good: Focused action
public struct HashPasswordAction: ActionImplementation { ... }

// Bad: Action doing too much
public struct UserManagementAction: ActionImplementation { ... }

Fail Fast with Descriptive Errors

public func execute(...) async throws -> any Sendable {
    // Validate required services
    guard let service = context.service(MyService.self) else {
        throw ActionError.serviceNotFound("MyService")
    }

    // Validate required variables
    let input: InputType = try context.require(result.identifier)

    // ... proceed with execution
}

Emit Events for Observability

// Emit events for significant operations
context.emit(PaymentProcessedEvent(
    amount: amount,
    currency: currency,
    transactionId: result.id
))

Example: External API Action

public struct WeatherAction: ActionImplementation {
    public static let role: ActionRole = .request
    public static let verbs: Set<String> = ["Weather", "Forecast"]
    public static let validPrepositions: Set<Preposition> = [.forPrep]

    public init() {}

    public func execute(
        result: ResultDescriptor,
        object: ObjectDescriptor,
        context: ExecutionContext
    ) async throws -> any Sendable {
        guard let httpClient = context.service(HTTPClientService.self) else {
            throw ActionError.serviceNotFound("HTTPClientService")
        }

        let city: String = try context.require(object.identifier)
        let url = "https://api.weather.com/v1/forecast?city=\(city)"
        let response = try await httpClient.get(url: url)

        context.bind(result.identifier, value: response)
        return response
    }
}

Usage:

<Weather> the <forecast> for the <city>.

Summary

Creating custom actions involves implementing the ActionImplementation protocol, defining role, verbs, and valid prepositions, implementing the async execute method, and registering with ActionRegistry.