@@ -3,6 +3,8 @@ title: First Simple Action
33description : Build a complete action with a function, event, and data type from scratch
44---
55
6+ import { Tabs , Tab } from ' fumadocs-ui/components/tabs' ;
7+
68This tutorial walks through building a complete action that:
79
810- Exposes a ` fibonacci ` function
@@ -11,8 +13,11 @@ This tutorial walks through building a complete action that:
1113
1214## 1. Create the RuntimeFunction
1315
14- A ` RuntimeFunction ` contains the actual implementation. Decorate it with ` @Identifier ` , ` @Signature ` , and ` @Name ` ,
15- then implement the ` run() ` method.
16+ A ` RuntimeFunction ` contains the actual implementation.
17+
18+ <Tabs items = { [' TypeScript' , ' Rust' ]} >
19+ <Tab value = " TypeScript" >
20+ Decorate it with ` @Identifier ` , ` @Signature ` , and ` @Name ` , then implement the ` run() ` method.
1621
1722``` ts
1823// src/functions/fibonacciRuntimeFunction.ts
@@ -47,11 +52,62 @@ export class FibonacciRuntimeFunction {
4752
4853` @OmitRuntimeFunction() ` prevents Hercules from auto-generating a public function definition from this class — we'll
4954define that separately below.
55+ </Tab >
56+ <Tab value = " Rust" >
57+ Attach ` #[hercules::runtime_function(...)] ` , giving it an ` identifier ` and ` signature ` , then implement
58+ ` RuntimeFunctionHandler ` . Attaching the macro registers it automatically — there's no separate registration call to
59+ make later.
60+
61+ ``` rust
62+ // src/functions/fibonacci_runtime_function.rs
63+ use hercules :: {
64+ async_trait, Arguments , FunctionContext , PlainValue , Result , RuntimeFunctionHandler ,
65+ };
66+
67+ #[hercules:: runtime_function(
68+ identifier = " fibonacci_runtime" ,
69+ signature = " (test: NUMBER): NUMBER" ,
70+ name(en_US = " Fibonacci (Runtime)" ),
71+ display_message(en_US = " Computes the n-th Fibonacci number" ),
72+ omit_definition
73+ )]
74+ #[parameter(runtime_name = " test" , name(en_US = " N" ))]
75+ pub struct FibonacciRuntimeFunction ;
76+
77+ #[async_trait]
78+ impl RuntimeFunctionHandler for FibonacciRuntimeFunction {
79+ async fn run (& self , context : & FunctionContext , args : & Arguments ) -> Result <PlainValue > {
80+ let n : u64 = args . get (" test" )? ;
81+
82+ log :: debug! (
83+ " computing fibonacci({n}) for project={} execution={}" ,
84+ context . project_id,
85+ context . execution_id
86+ );
87+ Ok (fib (n ). into ())
88+ }
89+ }
90+
91+ fn fib (n : u64 ) -> u64 {
92+ if n <= 1 {
93+ n
94+ } else {
95+ fib (n - 1 ) + fib (n - 2 )
96+ }
97+ }
98+ ```
99+
100+ ` omit_definition ` prevents Hercules from auto-publishing this runtime function as its own public function — we'll
101+ define that separately below.
102+ </Tab >
103+ </Tabs >
50104
51105## 2. Create the Function
52106
53107A ` Function ` extends the ` RuntimeFunction ` and adds public-facing metadata without changing the implementation.
54108
109+ <Tabs items = { [' TypeScript' , ' Rust' ]} >
110+ <Tab value = " TypeScript" >
55111``` ts
56112// src/functions/fibonacciFunction.ts
57113import { Identifier , Name , Parameter } from ' @code0-tech/hercules' ;
@@ -67,9 +123,31 @@ import { FibonacciRuntimeFunction } from './fibonacciRuntimeFunction.js';
67123})
68124export class FibonacciFunction extends FibonacciRuntimeFunction {}
69125```
126+ </Tab >
127+ <Tab value = " Rust" >
128+ ` #[hercules::function(base = ...)] ` plays the same role as ` extends ` here: it points at the runtime function that
129+ actually executes, and carries only metadata overrides — no ` run() ` of its own.
130+
131+ ``` rust
132+ // src/functions/fibonacci_function.rs
133+ use super :: fibonacci_runtime_function :: FibonacciRuntimeFunction ;
134+
135+ #[hercules:: function(base = FibonacciRuntimeFunction , identifier = " fibonacci" )]
136+ #[parameter(
137+ runtime_name = " test" ,
138+ name(en_US = " Input Number" ),
139+ description(en_US = " The position in the Fibonacci sequence" ),
140+ default_value = 10
141+ )]
142+ pub struct FibonacciFunction ;
143+ ```
144+ </Tab >
145+ </Tabs >
70146
71147## 3. Create the RuntimeEvent
72148
149+ <Tabs items = { [' TypeScript' , ' Rust' ]} >
150+ <Tab value = " TypeScript" >
73151``` ts
74152// src/events/userCreatedRuntimeEvent.ts
75153import { Identifier , Signature , Name , EventSetting } from ' @code0-tech/hercules' ;
@@ -85,9 +163,30 @@ import { Identifier, Signature, Name, EventSetting } from '@code0-tech/hercules'
85163})
86164export class UserCreatedRuntimeEvent {}
87165```
166+ </Tab >
167+ <Tab value = " Rust" >
168+ ``` rust
169+ // src/events/user_created_runtime_event.rs
170+ #[hercules:: runtime_event(
171+ identifier = " user_created" ,
172+ signature = " (userId: NUMBER): void" ,
173+ name(en_US = " User Created" )
174+ )]
175+ #[setting(
176+ identifier = " FILTER_ROLE" ,
177+ name(en_US = " Role Filter" ),
178+ description(en_US = " Only trigger for users with this role" ),
179+ optional
180+ )]
181+ pub struct UserCreatedRuntimeEvent ;
182+ ```
183+ </Tab >
184+ </Tabs >
88185
89186## 4. Create the Event
90187
188+ <Tabs items = { [' TypeScript' , ' Rust' ]} >
189+ <Tab value = " TypeScript" >
91190``` ts
92191// src/events/userCreatedEvent.ts
93192import { Identifier , Name , Editable } from ' @code0-tech/hercules' ;
@@ -98,9 +197,29 @@ import { UserCreatedRuntimeEvent } from './userCreatedRuntimeEvent.js';
98197@Editable (false )
99198export class UserCreatedEvent extends UserCreatedRuntimeEvent {}
100199```
200+ </Tab >
201+ <Tab value = " Rust" >
202+ ``` rust
203+ // src/events/user_created_event.rs
204+ use super :: user_created_runtime_event :: UserCreatedRuntimeEvent ;
205+
206+ #[hercules:: event(
207+ base = UserCreatedRuntimeEvent ,
208+ identifier = " user_created_event" ,
209+ name(en_US = " On User Created" )
210+ )]
211+ pub struct UserCreatedEvent ;
212+ ```
213+
214+ ` editable ` defaults to ` false ` , so there's nothing to set explicitly here — add the bare ` editable ` flag to turn it
215+ on.
216+ </Tab >
217+ </Tabs >
101218
102219## 5. Create the DataType
103220
221+ <Tabs items = { [' TypeScript' , ' Rust' ]} >
222+ <Tab value = " TypeScript" >
104223Data types are backed by a Zod schema. Decorate the class with ` @Identifier ` , ` @Name ` , and ` @Schema ` .
105224
106225``` ts
@@ -115,9 +234,28 @@ const EmailSchema = z.string().regex(/^[^@]+@[^@]+\.[^@]+$/);
115234@Schema (EmailSchema )
116235export class EmailDataType {}
117236```
237+ </Tab >
238+ <Tab value = " Rust" >
239+ Data types are derived from a real Rust type's ` schemars::JsonSchema ` impl instead of a hand-written schema DSL —
240+ ` schemars ` ' own validation attributes (` #[schemars(regex(...))] ` ) double as the wire validation rules.
241+
242+ ``` rust
243+ // src/data_types/email_data_type.rs
244+ use hercules :: JsonSchema ;
245+ use serde :: {Deserialize , Serialize };
246+
247+ #[hercules:: data_type(identifier = " email_address" , name(en_US = " Email Address" ))]
248+ #[derive(Debug , Clone , Serialize , Deserialize , JsonSchema )]
249+ #[serde(transparent)]
250+ pub struct EmailAddress (#[schemars(regex(pattern = r " ^[^@]+@[^@]+\.[^@]+$" ))] pub String );
251+ ```
252+ </Tab >
253+ </Tabs >
118254
119255## 6. Wire It All Together
120256
257+ <Tabs items = { [' TypeScript' , ' Rust' ]} >
258+ <Tab value = " TypeScript" >
121259``` ts
122260// src/index.ts
123261import { Action , CodeZeroEvent } from ' @code0-tech/hercules' ;
@@ -164,14 +302,86 @@ action.connect(process.env.AUTH_TOKEN ?? 'token').catch((err: unknown) => {
164302 process .exit (1 );
165303});
166304```
305+ </Tab >
306+ <Tab value = " Rust" >
307+ Every type above already registered itself when its module got linked into the binary, so there's nothing left to
308+ wire up beyond building the ` Action ` and connecting it:
309+
310+ ``` rust
311+ // src/main.rs
312+ mod data_types ;
313+ mod events ;
314+ mod functions ;
315+
316+ use hercules :: {Action , ConfigurationDefinition , HerculesEvent , Translation };
317+ use tokio_stream :: StreamExt ;
318+
319+ fn env (key : & str , default : & str ) -> String {
320+ std :: env :: var (key ). unwrap_or_else (| _ | default . to_string ())
321+ }
322+
323+ fn build_action () -> Action {
324+ Action :: new (env (" ACTION_ID" , " example-action" ), env (" VERSION" , " 0.0.0" ))
325+ . aquila_url (env (" AQUILA_URL" , " 127.0.0.1:8081" ))
326+ . author (" my-org" )
327+ . icon (" tabler:bolt" )
328+ . documentation (" A simple example action" )
329+ . name ([Translation :: new (" en-US" , " Example Action" )])
330+ . configuration (
331+ ConfigurationDefinition :: new (" EXAMPLE_CONFIG" , " string" )
332+ . name ([Translation :: new (" en-US" , " Example Config" )]),
333+ )
334+ }
335+
336+ #[tokio:: main]
337+ async fn main () -> hercules :: Result <()> {
338+ let action = build_action ();
339+ let mut events = action . subscribe ();
340+
341+ // The returned `Connected` handle can be discarded immediately: the
342+ // background dispatch task (started inside `connect`) holds its own
343+ // `Arc` to the shared connection state.
344+ action
345+ . connect (env (" AUTH_TOKEN" , " token" ), None )
346+ . await
347+ . unwrap_or_else (| err | panic! (" failed to connect to Aquila: {err}" ));
348+
349+ // React to connection lifecycle events on the main task for as long as
350+ // the process runs. Panicking here (unlike inside a spawned task) takes
351+ // the whole process down on an unrecoverable stream error.
352+ while let Some (event ) = events . next (). await {
353+ match event {
354+ HerculesEvent :: Connected => log :: info! (" connected to Aquila" ),
355+ HerculesEvent :: Error (error ) => panic! (" Aquila stream error: {error}" ),
356+ _ => {}
357+ }
358+ }
359+
360+ Ok (())
361+ }
362+ ```
363+ </Tab >
364+ </Tabs >
167365
168366## Firing an Event
169367
170368To fire the ` user_created ` event from within your action (e.g. after a webhook call):
171369
370+ <Tabs items = { [' TypeScript' , ' Rust' ]} >
371+ <Tab value = " TypeScript" >
172372``` ts
173373await action .fire (UserCreatedRuntimeEvent , projectId , { userId: 42 });
174374```
175375
176376The second argument is the project ID this event belongs to, and the third is the payload matching the event's
177377signature.
378+ </Tab >
379+ <Tab value = " Rust" >
380+ ``` rust
381+ action . fire (" user_created" , project_id , serde_json :: json! ({ " userId" : 42 }))? ;
382+ ```
383+
384+ The second argument is the project ID this event belongs to, and the third is the payload matching the event's
385+ signature.
386+ </Tab >
387+ </Tabs >
0 commit comments