Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,27 @@ jobs:
- name: Run tests
run: npm run test

rust:
runs-on: ubuntu-latest

defaults:
run:
shell: bash
working-directory: rust

steps:
- uses: actions/checkout@v6
- name: Set up Rust
uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
- name: Cache cargo
uses: Swatinem/rust-cache@v2
with:
workspaces: rust
- name: Check formatting
run: cargo fmt --all -- --check
- name: Lint
run: cargo clippy --workspace --all-targets -- -D warnings
- name: Run tests
run: cargo test --workspace
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ The action sdk to connect with aquila

This action sdk is currently implemented in:
- [Typescript](./ts/README.md)
- [Rust](./rust/README.md)

# GRPC Communcation Flow

Expand Down
2 changes: 1 addition & 1 deletion docs/getting-started/introduction.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ An action is a service **you** build and run. It connects to the Code0 runtime (
what functions it provides, what events it can fire, and what custom data types it understands.
Users then pick your functions and events inside the flow editor.

Hercules is the TypeScript SDK for building actions.
Hercules is the SDK for building actions, available for TypeScript and Rust.

## What an Action does

Expand Down
214 changes: 212 additions & 2 deletions docs/tutorials/first-simple-action.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ 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
Expand All @@ -11,8 +13,11 @@ This tutorial walks through building a complete action that:

## 1. Create the RuntimeFunction

A `RuntimeFunction` contains the actual implementation. Decorate it with `@Identifier`, `@Signature`, and `@Name`,
then implement the `run()` method.
A `RuntimeFunction` contains the actual implementation.

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

```ts
// src/functions/fibonacciRuntimeFunction.ts
Expand Down Expand Up @@ -47,11 +52,62 @@ export class FibonacciRuntimeFunction {

`@OmitRuntimeFunction()` prevents Hercules from auto-generating a public function definition from this class — we'll
define that separately below.
</Tab>
<Tab value="Rust">
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.

```rust
// 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.
</Tab>
</Tabs>

## 2. Create the Function

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

<Tabs items={['TypeScript', 'Rust']}>
<Tab value="TypeScript">
```ts
// src/functions/fibonacciFunction.ts
import { Identifier, Name, Parameter } from '@code0-tech/hercules';
Expand All @@ -67,9 +123,31 @@ import { FibonacciRuntimeFunction } from './fibonacciRuntimeFunction.js';
})
export class FibonacciFunction extends FibonacciRuntimeFunction {}
```
</Tab>
<Tab value="Rust">
`#[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.

```rust
// 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;
```
</Tab>
</Tabs>

## 3. Create the RuntimeEvent

<Tabs items={['TypeScript', 'Rust']}>
<Tab value="TypeScript">
```ts
// src/events/userCreatedRuntimeEvent.ts
import { Identifier, Signature, Name, EventSetting } from '@code0-tech/hercules';
Expand All @@ -85,9 +163,30 @@ import { Identifier, Signature, Name, EventSetting } from '@code0-tech/hercules'
})
export class UserCreatedRuntimeEvent {}
```
</Tab>
<Tab value="Rust">
```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;
```
</Tab>
</Tabs>

## 4. Create the Event

<Tabs items={['TypeScript', 'Rust']}>
<Tab value="TypeScript">
```ts
// src/events/userCreatedEvent.ts
import { Identifier, Name, Editable } from '@code0-tech/hercules';
Expand All @@ -98,9 +197,29 @@ import { UserCreatedRuntimeEvent } from './userCreatedRuntimeEvent.js';
@Editable(false)
export class UserCreatedEvent extends UserCreatedRuntimeEvent {}
```
</Tab>
<Tab value="Rust">
```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
Expand All @@ -115,9 +234,28 @@ const EmailSchema = z.string().regex(/^[^@]+@[^@]+\.[^@]+$/);
@Schema(EmailSchema)
export class EmailDataType {}
```
</Tab>
<Tab value="Rust">
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.

```rust
// 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);
```
</Tab>
</Tabs>

## 6. Wire It All Together

<Tabs items={['TypeScript', 'Rust']}>
<Tab value="TypeScript">
```ts
// src/index.ts
import { Action, CodeZeroEvent } from '@code0-tech/hercules';
Expand Down Expand Up @@ -164,14 +302,86 @@ action.connect(process.env.AUTH_TOKEN ?? 'token').catch((err: unknown) => {
process.exit(1);
});
```
</Tab>
<Tab value="Rust">
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:

```rust
// 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(())
}
```
</Tab>
</Tabs>

## Firing an Event

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

<Tabs items={['TypeScript', 'Rust']}>
<Tab value="TypeScript">
```ts
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.
</Tab>
<Tab value="Rust">
```rust
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.
</Tab>
</Tabs>
1 change: 1 addition & 0 deletions rust/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/target
Loading