Skip to content

Latest commit

 

History

History
387 lines (330 loc) · 11.4 KB

File metadata and controls

387 lines (330 loc) · 11.4 KB
title First Simple Action
description Build a complete action with a function, event, and data type from scratch

import { Tabs, Tab } from 'fumadocs-ui/components/tabs';

This tutorial walks through building a complete action that:

  • Exposes a fibonacci function
  • Fires a user_created event
  • Registers an email_address data type

1. Create the RuntimeFunction

A RuntimeFunction contains the actual implementation.

<Tabs items={['TypeScript', 'Rust']}> Decorate it with @Identifier, @Signature, and @Name, then implement the run() method.

// src/functions/fibonacciRuntimeFunction.ts
import {
  Identifier,
  Signature,
  Name,
  DisplayMessage,
  Parameter,
  OmitRuntimeFunction,
  FunctionContext,
} from '@code0-tech/hercules';

@Identifier('fibonacci_runtime')
@Signature('(test: number): number')
@Name({ code: 'en-US', content: 'Fibonacci (Runtime)' })
@DisplayMessage({ code: 'en-US', content: 'Computes the n-th Fibonacci number' })
@OmitRuntimeFunction()
@Parameter({ runtimeName: 'test', name: [{ code: 'en-US', content: 'N' }] })
export class FibonacciRuntimeFunction {
  run(context: FunctionContext, test: number): number {
    console.log(`[fibonacci] project=${context.projectId} execution=${context.executionId}`);
    return this.fib(test);
  }

  private fib(n: number): number {
    if (n <= 1) return n;
    return this.fib(n - 1) + this.fib(n - 2);
  }
}

@OmitRuntimeFunction() prevents Hercules from auto-generating a public function definition from this class — we'll define that separately below. Attach #[hercules::runtime_function(...)], giving it an identifier and signature, then implement RuntimeFunctionHandler. Attaching the macro registers it automatically — there's no separate registration call to make later.

// src/functions/fibonacci_runtime_function.rs
use hercules::{
    async_trait, Arguments, FunctionContext, PlainValue, Result, RuntimeFunctionHandler,
};

#[hercules::runtime_function(
    identifier = "fibonacci_runtime",
    signature = "(test: NUMBER): NUMBER",
    name(en_US = "Fibonacci (Runtime)"),
    display_message(en_US = "Computes the n-th Fibonacci number"),
    omit_definition
)]
#[parameter(runtime_name = "test", name(en_US = "N"))]
pub struct FibonacciRuntimeFunction;

#[async_trait]
impl RuntimeFunctionHandler for FibonacciRuntimeFunction {
    async fn run(&self, context: &FunctionContext, args: &Arguments) -> Result<PlainValue> {
        let n: u64 = args.get("test")?;

        log::debug!(
            "computing fibonacci({n}) for project={} execution={}",
            context.project_id,
            context.execution_id
        );
        Ok(fib(n).into())
    }
}

fn fib(n: u64) -> u64 {
    if n <= 1 {
        n
    } else {
        fib(n - 1) + fib(n - 2)
    }
}

omit_definition prevents Hercules from auto-publishing this runtime function as its own public function — we'll define that separately below.

2. Create the Function

A Function extends the RuntimeFunction and adds public-facing metadata without changing the implementation.

<Tabs items={['TypeScript', 'Rust']}>

// src/functions/fibonacciFunction.ts
import { Identifier, Name, Parameter } from '@code0-tech/hercules';
import { FibonacciRuntimeFunction } from './fibonacciRuntimeFunction.js';

@Identifier('fibonacci')
@Name({ code: 'en-US', content: 'Compute Fibonacci Number' })
@Parameter({
  runtimeName: 'test',
  name: [{ code: 'en-US', content: 'Input Number' }],
  description: [{ code: 'en-US', content: 'The position in the Fibonacci sequence' }],
  defaultValue: 10,
})
export class FibonacciFunction extends FibonacciRuntimeFunction {}
`#[hercules::function(base = ...)]` plays the same role as `extends` here: it points at the runtime function that actually executes, and carries only metadata overrides — no `run()` of its own.
// src/functions/fibonacci_function.rs
use super::fibonacci_runtime_function::FibonacciRuntimeFunction;

#[hercules::function(base = FibonacciRuntimeFunction, identifier = "fibonacci")]
#[parameter(
    runtime_name = "test",
    name(en_US = "Input Number"),
    description(en_US = "The position in the Fibonacci sequence"),
    default_value = 10
)]
pub struct FibonacciFunction;

3. Create the RuntimeEvent

<Tabs items={['TypeScript', 'Rust']}>

// src/events/userCreatedRuntimeEvent.ts
import { Identifier, Signature, Name, EventSetting } from '@code0-tech/hercules';

@Identifier('user_created')
@Signature('(userId: number): void')
@Name({ code: 'en-US', content: 'User Created' })
@EventSetting({
  identifier: 'FILTER_ROLE',
  name: [{ code: 'en-US', content: 'Role Filter' }],
  description: [{ code: 'en-US', content: 'Only trigger for users with this role' }],
  optional: true,
})
export class UserCreatedRuntimeEvent {}
```rust // src/events/user_created_runtime_event.rs #[hercules::runtime_event( identifier = "user_created", signature = "(userId: NUMBER): void", name(en_US = "User Created") )] #[setting( identifier = "FILTER_ROLE", name(en_US = "Role Filter"), description(en_US = "Only trigger for users with this role"), optional )] pub struct UserCreatedRuntimeEvent; ```

4. Create the Event

<Tabs items={['TypeScript', 'Rust']}>

// src/events/userCreatedEvent.ts
import { Identifier, Name, Editable } from '@code0-tech/hercules';
import { UserCreatedRuntimeEvent } from './userCreatedRuntimeEvent.js';

@Identifier('user_created_event')
@Name({ code: 'en-US', content: 'On User Created' })
@Editable(false)
export class UserCreatedEvent extends UserCreatedRuntimeEvent {}
```rust // src/events/user_created_event.rs use super::user_created_runtime_event::UserCreatedRuntimeEvent;

#[hercules::event( base = UserCreatedRuntimeEvent, identifier = "user_created_event", name(en_US = "On User Created") )] pub struct UserCreatedEvent;


`editable` defaults to `false`, so there's nothing to set explicitly here — add the bare `editable` flag to turn it
on.
</Tab>
</Tabs>

## 5. Create the DataType

<Tabs items={['TypeScript', 'Rust']}>
<Tab value="TypeScript">
Data types are backed by a Zod schema. Decorate the class with `@Identifier`, `@Name`, and `@Schema`.

```ts
// src/data_types/emailDataType.ts
import { Identifier, Name, Schema } from '@code0-tech/hercules';
import { z } from 'zod';

const EmailSchema = z.string().regex(/^[^@]+@[^@]+\.[^@]+$/);

@Identifier('email_address')
@Name({ code: 'en-US', content: 'Email Address' })
@Schema(EmailSchema)
export class EmailDataType {}
Data types are derived from a real Rust type's `schemars::JsonSchema` impl instead of a hand-written schema DSL — `schemars`' own validation attributes (`#[schemars(regex(...))]`) double as the wire validation rules.
// src/data_types/email_data_type.rs
use hercules::JsonSchema;
use serde::{Deserialize, Serialize};

#[hercules::data_type(identifier = "email_address", name(en_US = "Email Address"))]
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(transparent)]
pub struct EmailAddress(#[schemars(regex(pattern = r"^[^@]+@[^@]+\.[^@]+$"))] pub String);

6. Wire It All Together

<Tabs items={['TypeScript', 'Rust']}>

// src/index.ts
import { Action, CodeZeroEvent } from '@code0-tech/hercules';
import { FibonacciRuntimeFunction } from './functions/fibonacciRuntimeFunction.js';
import { FibonacciFunction } from './functions/fibonacciFunction.js';
import { UserCreatedRuntimeEvent } from './events/userCreatedRuntimeEvent.js';
import { UserCreatedEvent } from './events/userCreatedEvent.js';
import { EmailDataType } from './data_types/emailDataType.js';

const action = new Action(
  process.env.ACTION_ID ?? 'example-action',
  process.env.VERSION ?? '0.0.0',
  process.env.AQUILA_URL ?? '127.0.0.1:8081',
  'my-org',
  'tabler:bolt',
  'A simple example action',
  [{ code: 'en-US', content: 'Example Action' }],
  [
    {
      identifier: 'EXAMPLE_CONFIG',
      type: 'string',
      name: [{ code: 'en-US', content: 'Example Config' }],
    },
  ],
);

action.registerRuntimeFunction(FibonacciRuntimeFunction);
action.registerFunction(FibonacciFunction);
action.registerDataTypeClass(EmailDataType);
action.registerRuntimeEventClass(UserCreatedRuntimeEvent);
action.registerEventClass(UserCreatedEvent);

action.on(CodeZeroEvent.connected, () => {
  console.log('Connected to Aquila');
});

action.on(CodeZeroEvent.error, (error: Error) => {
  console.error('Stream error:', error.message);
  process.exit(1);
});

action.connect(process.env.AUTH_TOKEN ?? 'token').catch((err: unknown) => {
  console.error('Failed to connect:', err);
  process.exit(1);
});
Every type above already registered itself when its module got linked into the binary, so there's nothing left to wire up beyond building the `Action` and connecting it:
// src/main.rs
mod data_types;
mod events;
mod functions;

use hercules::{Action, ConfigurationDefinition, HerculesEvent, Translation};
use tokio_stream::StreamExt;

fn env(key: &str, default: &str) -> String {
    std::env::var(key).unwrap_or_else(|_| default.to_string())
}

fn build_action() -> Action {
    Action::new(env("ACTION_ID", "example-action"), env("VERSION", "0.0.0"))
        .aquila_url(env("AQUILA_URL", "127.0.0.1:8081"))
        .author("my-org")
        .icon("tabler:bolt")
        .documentation("A simple example action")
        .name([Translation::new("en-US", "Example Action")])
        .configuration(
            ConfigurationDefinition::new("EXAMPLE_CONFIG", "string")
                .name([Translation::new("en-US", "Example Config")]),
        )
}

#[tokio::main]
async fn main() -> hercules::Result<()> {
    let action = build_action();
    let mut events = action.subscribe();

    // The returned `Connected` handle can be discarded immediately: the
    // background dispatch task (started inside `connect`) holds its own
    // `Arc` to the shared connection state.
    action
        .connect(env("AUTH_TOKEN", "token"), None)
        .await
        .unwrap_or_else(|err| panic!("failed to connect to Aquila: {err}"));

    // React to connection lifecycle events on the main task for as long as
    // the process runs. Panicking here (unlike inside a spawned task) takes
    // the whole process down on an unrecoverable stream error.
    while let Some(event) = events.next().await {
        match event {
            HerculesEvent::Connected => log::info!("connected to Aquila"),
            HerculesEvent::Error(error) => panic!("Aquila stream error: {error}"),
            _ => {}
        }
    }

    Ok(())
}

Firing an Event

To fire the user_created event from within your action (e.g. after a webhook call):

<Tabs items={['TypeScript', 'Rust']}>

await action.fire(UserCreatedRuntimeEvent, projectId, { userId: 42 });

The second argument is the project ID this event belongs to, and the third is the payload matching the event's signature.

action.fire("user_created", project_id, serde_json::json!({ "userId": 42 }))?;

The second argument is the project ID this event belongs to, and the third is the payload matching the event's signature.