# Robomotion Rust Package Development Guide

*Last updated: December 18, 2025*

Welcome to the **Robomotion package development guide** for Rust developers. This guide covers building custom Robomotion packages using the [`robomotion-rust`](https://github.com/robomotionio/robomotion-rust) SDK.

---

## 1. Prerequisites

| Tool | Minimum version | Purpose |
|------|-----------------|---------|
| Rust | `1.70` | Compiles your code & provides `cargo` tooling |
| `roboctl` | **latest** (run `roboctl version`) | CLI that scaffolds and builds packages |
| Git | any | Source-control & versioning |
| A text editor / IDE | – | Coding |

> **Install `roboctl`**
> Since roboctl is not open source, download the binary for your platform:
> ```bash
> # Download for your platform:
> # macOS Intel: https://packages.robomotion.io/releases/roboctl/roboctl-v1.8.0-darwin-amd64.tar.gz
> # macOS Apple Silicon: https://packages.robomotion.io/releases/roboctl/roboctl-v1.8.0-darwin-arm64.tar.gz
> # Linux: https://packages.robomotion.io/releases/roboctl/roboctl-v1.8.0-linux-amd64.tar.gz
> # Windows: https://packages.robomotion.io/releases/roboctl/roboctl-v1.8.0-windows-amd64.tar.gz
>
> # Extract and add to PATH
> tar -xzf roboctl-v1.8.0-*.tar.gz
> sudo mv roboctl /usr/local/bin/  # or add to your PATH
> ```

---

## 2. Package Anatomy – What gets shipped

A Robomotion *package* is nothing more than **a self-contained binary plus a metadata file**. When imported into a robot the runner spawns the binary and talks to it via gRPC (the plumbing is already handled by the SDK).

```
my-package/
 ├─ Cargo.toml           # Rust package definition
 ├─ config.json          # Metadata & build scripts        ← required
 ├─ src/
 │   ├─ main.rs          # Entry-point: register nodes
 │   └─ v1/              # Your first version of nodes
 │       ├─ mod.rs       # Module exports
 │       ├─ hello.rs     # Implementation of a node
 │       └─ world.rs
 ├─ icon.png             # Package icon shown in Designer  ← optional (PNG/SVG)
 └─ dist/                # Artefacts created by roboctl
```

### `config.json`
The file is conceptually similar to `package.json` in NodeJS – *it is authoritative*. Below is an abbreviated example:

```json
{
  "name": "Chat Assistant",
  "namespace": "Robomotion.ChatAssistant",
  "version": "1.1.4",
  "keywords": ["chat", "agent", "ai"],
  "categories": ["Productivity", "AI"],
  "description": "Create powerful AI chat assistants…",
  "icon": "icon.png",
  "language": "Rust",
  "platforms": ["linux", "windows", "darwin"],
  "author": { "name": "You", "email": "you@example.com" },
  "scripts": {
    "linux":   { "build": ["cargo build --release && cp target/release/my-package dist/"], "run": "my-package" },
    "windows": { "build": ["cargo build --release && copy target\\release\\my-package.exe dist\\"], "run": "my-package.exe" },
    "darwin":  { "build": ["cargo build --release && cp target/release/my-package dist/"], "run": "my-package" }
  }
}
```
The `scripts.<os>.build` array can run **any** shell command – but 90% of the time one `cargo build --release` line is enough.

---

## 3. Bootstrap a New Package (recommended way)

```bash
roboctl package create \
  --name "Weather" \
  --namespace "Acme.Weather" \
  --description "Fetches weather data" \
  --categories "Utilities" \
  --color "#3498db" \
  --language Rust \
  -o weather
cd weather
```
The wizard sets up **everything** – including `config.json`, a minimal `main.rs`, the icon stub and Git ignore.

> **Manual route** – If you prefer full control, create the files yourself; just respect the anatomy from section 2.

---

## 4. Understanding the Runtime

The Robomotion runtime is exposed through the **`robomotion`** crate. A few key concepts:

| Concept | Type | Description |
|---------|------|-------------|
| *Node* | `NodeBase` | Struct that carries common fields (GUID, delays, …) |
| *Variable* | `InVariable<T>`, `OutVariable<T>`, `OptVariable<T>` | Strongly-typed message variables |
| *Lifecycle* | `on_create`, `on_message`, `on_close` | Mandatory async callbacks a node must implement |
| *MessageHandler* | trait `MessageHandler` | Trait all nodes implement for lifecycle methods |
| *Large Message Object (LMO)* | `LargeMessageObject` | Mechanism to transport objects >256 KB |

You **never** instantiate a node yourself – the runner does that through the factory system.

---

## 5. Declaring a Node (the derive macro)

Every Rust struct that derives `Node` and is registered in `main.rs` becomes a *node* in Designer.
Metadata is provided via **attribute macros**.

> **Important**: The node `id` must start with the `namespace` from your `config.json`. For example, if your `config.json` has `"namespace": "Acme"`, all your node IDs must start with `Acme.` (e.g., `Acme.Hello`, `Acme.World`).

```rust
// src/v1/hello.rs
// Assumes config.json has: "namespace": "Acme"
use robomotion::prelude::*;
use robomotion::message::Context;

#[derive(Node, Default)]
#[node(id = "Acme.Hello", name = "Hello", icon = "mdiHand", color = "#3498db", inputs = 1, outputs = 1)]
pub struct Hello {
    #[serde(flatten)]
    pub node: NodeBase,

    // === INPUT ==========================================================
    #[input(title = "Name", var_type = "string", scope = "Message", name = "name")]
    pub in_name: InVariable<String>,

    // === OUTPUT ==========================================================
    #[output(title = "Greeting", var_type = "string", scope = "Message", name = "greeting")]
    pub out_greeting: OutVariable<String>,

    // === OPTIONS =========================================================
    #[option(title = "Default Greeting", var_type = "string", value = "Hello")]
    pub opt_greeting: String,
}

#[async_trait]
impl MessageHandler for Hello {
    async fn on_create(&mut self) -> Result<()> {
        Ok(())
    }

    async fn on_message(&mut self, ctx: &mut Context) -> Result<()> {
        let name = self.in_name.get(ctx).await?;
        let greeting = format!("{} {}", self.opt_greeting, name);
        self.out_greeting.set(ctx, greeting).await?;
        Ok(())
    }

    async fn on_close(&mut self) -> Result<()> {
        Ok(())
    }
}
```

### 5.1 Attribute reference (fields & node)

Below is a **non-exhaustive** but practically complete list of attributes you can use:

#### Node Attributes (`#[node(...)]`)

| Key | Example | Meaning |
|-----|---------|---------|
| `id` | `Acme.Hello` | **Unique** identifier that the runtime looks up. **MUST start with the namespace from config.json** |
| `name` | `Hello` | Display name in Designer |
| `icon` | `mdiHand` | Material-Design-Icon identifier |
| `color` | `#3498db` | Hex color shown behind the icon. **IMPORTANT: All nodes in the same package MUST use the same color code** |
| `inputs` / `outputs` | `inputs = 0` | Override default 1-in 1-out configuration |

#### Node ID Namespace Requirements

**CRITICAL**: All node IDs **MUST** be prefixed with the `namespace` defined in your `config.json` file.

The namespace in `config.json` is the authoritative prefix for all node IDs in your package. This ensures proper node discovery and prevents ID conflicts across packages.

**Rules:**
1. The node ID must start with the exact namespace from `config.json`
2. After the namespace, add a dot (`.`) followed by the node name
3. You can optionally add a category between the namespace and node name: `Namespace.Category.NodeName`

**Example with `config.json`:**
```json
{
  "namespace": "Robomotion.LanceDB",
  "name": "LanceDB",
  "version": "1.0.0"
}
```

**Valid node IDs for this namespace:**
```rust
// Direct node name after namespace
#[node(id = "Robomotion.LanceDB.Connect", name = "Connect", ...)]

// With category
#[node(id = "Robomotion.LanceDB.Vector.Search", name = "Vector Search", ...)]
#[node(id = "Robomotion.LanceDB.Vector.Insert", name = "Vector Insert", ...)]

// More examples
#[node(id = "Robomotion.LanceDB.Query", name = "Query", ...)]
#[node(id = "Robomotion.LanceDB.Table.Create", name = "Create Table", ...)]
```

**INVALID node IDs (will not work):**
```rust
// Wrong - different namespace prefix
#[node(id = "LanceDB.Connect", ...)]           // ❌ Missing "Robomotion." prefix
#[node(id = "Acme.LanceDB.Connect", ...)]      // ❌ Wrong namespace prefix
#[node(id = "Connect", ...)]                   // ❌ No namespace at all

// Correct
#[node(id = "Robomotion.LanceDB.Connect", ...)] // ✅ Matches config.json namespace
```

**More namespace examples:**

| config.json namespace | Valid node IDs |
|-----------------------|----------------|
| `Acme.Weather` | `Acme.Weather.GetForecast`, `Acme.Weather.Current` |
| `MyCompany.Slack` | `MyCompany.Slack.SendMessage`, `MyCompany.Slack.Channel.List` |
| `OpenAI` | `OpenAI.ChatCompletion`, `OpenAI.Embedding.Create` |
| `AWS.S3` | `AWS.S3.Upload`, `AWS.S3.Download`, `AWS.S3.Bucket.List` |

#### Input Attributes (`#[input(...)]`)

| Key | Example | Meaning |
|-----|---------|---------|
| `title` | `Greeting` | Human friendly caption |
| `var_type` | `string` | Primitive type (`string`, `int`, `object`, …) |
| `scope` | `Message` | One of `Message`, `Custom`, `AI` |
| `name` | `greeting` | Variable name in message context |
| `description` | `The greeting message` | Tooltip text |

#### Output Attributes (`#[output(...)]`)

| Key | Example | Meaning |
|-----|---------|---------|
| `title` | `Result` | Human friendly caption |
| `var_type` | `string` | Primitive type |
| `scope` | `Message` | Scope for output |
| `name` | `result` | Variable name in message context |

#### Option Attributes (`#[option(...)]`)

| Key | Example | Meaning |
|-----|---------|---------|
| `title` | `Timeout` | Human friendly caption |
| `var_type` | `int` | Primitive type |
| `value` | `30` | Default value |
| `description` | `Timeout in seconds` | Tooltip text |

### 5.2 Package Color Consistency Requirements

**CRITICAL**: All nodes within the same package MUST use identical color codes to maintain visual consistency in the Flow Designer.

**Implementation Rules:**
- Choose ONE hex color code for your entire package (e.g., `#3498db`)
- Apply this exact color code to ALL nodes in your package
- Never mix different colors within a single package
- The color should represent the service/platform your package integrates with

**Example Implementation:**
```rust
// ALL nodes in the package use the same color
#[derive(Node, Default)]
#[node(id = "Slack.Connect", name = "Connect", icon = "mdiConnection", color = "#4A154B")]
pub struct ConnectNode { /* ... */ }

#[derive(Node, Default)]
#[node(id = "Slack.SendMessage", name = "Send Message", icon = "mdiMessage", color = "#4A154B")]
pub struct SendMessageNode { /* ... */ }

#[derive(Node, Default)]
#[node(id = "Slack.GetChannels", name = "Get Channels", icon = "mdiFormatListBulleted", color = "#4A154B")]
pub struct GetChannelsNode { /* ... */ }
```

**Common Color Choices:**
- **Slack**: `#4A154B` (Slack brand purple)
- **Notion**: `#000000` (Notion brand black)
- **GitHub**: `#333333` (GitHub dark gray)
- **AWS**: `#FF9900` (AWS orange)
- **Azure**: `#0078D4` (Microsoft blue)

### 5.3 Node Parameter Naming Rules

**CRITICAL**: Follow these standardized naming conventions for all node parameters:

#### Variable Naming Requirements

1. **InVariable names always start with `in_`** followed by snake_case:
   ```rust
   #[input(title = "Page Title", var_type = "string", scope = "Message", name = "pageTitle")]
   pub in_page_title: InVariable<String>,

   #[input(title = "Database ID", var_type = "string", scope = "Message", name = "databaseId")]
   pub in_database_id: InVariable<String>,
   ```

2. **OutVariable names always start with `out_`** followed by snake_case:
   ```rust
   #[output(title = "Result", var_type = "string", scope = "Message", name = "result")]
   pub out_result: OutVariable<String>,

   #[output(title = "Page ID", var_type = "string", scope = "Message", name = "pageId")]
   pub out_page_id: OutVariable<String>,
   ```

3. **OptVariable names always start with `opt_`** followed by snake_case:
   ```rust
   #[option(title = "Timeout", var_type = "int", value = "30")]
   pub opt_timeout: OptVariable<i32>,
   ```

4. **For variables with `scope = "Message"`, use camelCase for the `name` attribute**:
   ```rust
   // Correct examples
   #[input(title = "Page Title", var_type = "string", scope = "Message", name = "pageTitle")]
   pub in_page_title: InVariable<String>,

   #[output(title = "Result", var_type = "object", scope = "Message", name = "result")]
   pub out_result: OutVariable<serde_json::Value>,
   ```

### 5.4 Variables cheat-sheet

| Use-case | Type | Example | Naming Pattern |
|----------|------|---------|----------------|
| **Mandatory input** | `InVariable<T>` | `in_page_title: InVariable<String>` | `in_` + snake_case |
| **Optional input** | `OptVariable<T>` | `opt_timeout: OptVariable<i32>` | `opt_` + snake_case |
| **Output** | `OutVariable<T>` | `out_result: OutVariable<String>` | `out_` + snake_case |
| **Credential** | `Credential` | `opt_token: Credential` | `opt_` + snake_case |

All variable wrappers expose `.get(ctx).await` (for `In`/`Opt`/`Credential`) and `.set(ctx, val).await` (`Out`).

### 5.5 Type Mapping

| Designer Type | Rust Type |
|---------------|-----------|
| `string` | `String` |
| `int` | `i64` |
| `number` | `f64` |
| `boolean` | `bool` |
| `object` | `serde_json::Value` |
| `array` | `Vec<serde_json::Value>` |

---

## 6. Node Lifecycle

1. **on_create** – called exactly once when the flow starts or the node is (re-)deployed. Heavy initialization goes here.
2. **on_message** – called for every incoming token.
   • Retrieve inputs via variables.
   • Call external APIs.
   • Emit outputs by setting `OutVariable`s.
   • Return an error to stop the entire flow **unless** the node property `continue_on_error` is `true`.
3. **on_close** – counterpart to *on_create*. Close files, flush buffers, etc.

Delays can be added via `delay_before` and `delay_after` (in seconds) on the `NodeBase` – especially useful for rate-limited APIs.

---

## 7. Working with Credentials & RPA Vault

Robomotion includes a secure **RPA Vault** system for managing sensitive data like API tokens, passwords, database credentials, and certificates.

### 7.1 Declaring a Credential Field

```rust
#[derive(Node, Default)]
#[node(id = "Acme.MyAPI", name = "My API", icon = "mdiApi", color = "#3498db")]
pub struct MyAPINode {
    #[serde(flatten)]
    pub node: NodeBase,

    // Credential field - allows user to select from vault
    pub opt_token: Credential,
}
```

### 7.2 Accessing Vault Items

```rust
#[async_trait]
impl MessageHandler for MyAPINode {
    async fn on_message(&mut self, ctx: &mut Context) -> Result<()> {
        // Retrieve the vault item
        let item = self.opt_token.get(ctx).await?;

        // Extract the token value
        let token = item.get("value")
            .and_then(|v| v.as_str())
            .ok_or_else(|| RobomotionError::Variable("No Token Value".to_string()))?;

        // Use the token for API calls
        let client = reqwest::Client::new();
        let response = client
            .get("https://api.example.com/data")
            .header("Authorization", format!("Bearer {}", token))
            .send()
            .await?;

        // ... handle response
        Ok(())
    }
}
```

### 7.3 Credential Types & Item Structure

The vault supports 8 different credential types:

| Type | Category | Common Keys | Usage |
|------|----------|-------------|-------|
| **Login** | 1 | `username`, `password` | HTTP auth, FTP, browser automation |
| **Email** | 2 | `inbox.username`, `inbox.password`, `smtp.server`, `smtp.port` | Email operations |
| **Credit Card** | 3 | `cardholder`, `cardnumber`, `cvv`, `expDate` | Payment processing |
| **API Key/Token** | 4 | `value` | API authentication |
| **Database** | 5 | `server`, `port`, `database`, `username`, `password` | Database connections |
| **Document** | 6 | `filename`, `content` | OAuth tokens, certificates |
| **AES Key** | 7 | `value` | Encryption/decryption |
| **RSA Key Pair** | 8 | `publicKey`, `privateKey` | Cryptographic operations |

### 7.4 Common Usage Patterns

**API Token (most common):**
```rust
let item = self.opt_token.get(ctx).await?;
let token = item["value"].as_str().unwrap_or_default();
```

**Username/Password:**
```rust
let item = self.opt_login.get(ctx).await?;
let username = item["username"].as_str().unwrap_or_default();
let password = item["password"].as_str().unwrap_or_default();
```

**Database Connection:**
```rust
let item = self.opt_database.get(ctx).await?;
let server = item["server"].as_str().unwrap_or_default();
let port = item["port"].as_i64().unwrap_or(5432) as u16;
let database = item["database"].as_str().unwrap_or_default();
let username = item["username"].as_str().unwrap_or_default();
let password = item["password"].as_str().unwrap_or_default();
```

---

## 8. Registering & Starting

`main.rs` is straightforward – register every node and let the runtime take control:

```rust
use robomotion::prelude::*;

mod v1;

#[tokio::main]
async fn main() {
    // Register all nodes
    register_node::<v1::Hello>();
    register_node::<v1::World>();

    // Start the runtime
    start().await;
}
```

> **Multiple versions** – You can have `v2/`, `v3/` modules. Just import and register them side by side.

---

## 9. Build & Run locally

```bash
# Build the binary for your host OS
cargo build --release

# Or using roboctl
roboctl package build

# Run it inside a Robot (assumes you are developing *inside* a robot)
./target/release/my-package -a   # "attach" – streams logs & debugging to Designer
```

### 9.1 Generate *pspec* only
Sometimes you just want to refresh the Designer specification (JSON) without rebuilding everything:

```bash
./target/release/my-package -s   # outputs <namespace>-<version>.pspec to stdout
```

### 9.2 Cross-compiling & multi-arch builds

Need packages for Windows, macOS and different CPU architectures?

```bash
# Build for Windows on a Linux workstation
cargo build --release --target x86_64-pc-windows-gnu

# Build for Apple silicon (macOS arm64)
cargo build --release --target aarch64-apple-darwin

# Or using roboctl
roboctl package build --arch windows/amd64
roboctl package build --arch darwin/arm64
```

---

## 10. Debugging & Logs (attach / detach)

During local development you often want to see **stdout/stderr** and runtime events inside the Robomotion **Designer**.

| CLI Flag | Meaning |
|----------|---------|
| `-a` | **Attach** – the process discovers the local robot, connects via gRPC and streams logs/errors |
| `-s` | **Spec** – outputs the package specification JSON |
| *(none)* | Run standalone – useful for unit tests |

During an attached session **the robot will prefer the already-running binary** over any version found in the repository.
Thus the workflow is:

```bash
# 1. Rebuild after code change
cargo build --release

# 2. Start (or restart) in attach mode
./target/release/my-package -a &   # keep it running in background

# 3. In Designer → run your flow that uses the package
#    The robot detects the listening plugin and routes messages to it.
```

Inside your node you can use the `tracing` crate for logging:

```rust
use tracing::info;

#[async_trait]
impl MessageHandler for Hello {
    async fn on_message(&mut self, ctx: &mut Context) -> Result<()> {
        info!("Hi there! incoming GUID={}", self.node.guid);
        // … business logic …
        Ok(())
    }
}
```

Anything written to `stderr` will appear in the **Console** panel of the Designer while the flow is running.

---

## 11. Error Handling

The SDK provides `RobomotionError` for standardized error handling:

```rust
use robomotion::runtime::RobomotionError;

async fn on_message(&mut self, ctx: &mut Context) -> Result<()> {
    let value = self.in_data.get(ctx).await?;

    if value.is_empty() {
        return Err(RobomotionError::Variable("Input data is empty".to_string()));
    }

    // Process data...
    Ok(())
}
```

### Error Types

| Error | Usage |
|-------|-------|
| `RobomotionError::Variable(msg)` | Variable access errors |
| `RobomotionError::HandlerNotFound(guid)` | Handler lookup failures |
| `RobomotionError::Lmo(msg)` | Large Message Object errors |
| `RobomotionError::NotInitialized` | Runtime not initialized |

---

## 12. Working with Large Data (LMO)

For payloads larger than 256KB, the SDK automatically handles Large Message Objects:

```rust
use robomotion::runtime::lmo;

// Check if a value is an LMO
if lmo::is_lmo(&value) {
    let lmo_obj = lmo::deserialize_lmo_from_map(value.as_object().unwrap()).await?;
    let actual_data = lmo_obj.data.unwrap_or(serde_json::Value::Null);
}

// Serialize large data to LMO
if let Some(lmo) = lmo::serialize_lmo(&large_data).await? {
    // Data was stored as LMO
}
```

---

## 13. AI Tool Support

For nodes that should be usable as AI tools, add the `#[tool(...)]` attribute:

```rust
#[derive(Node, Default)]
#[node(id = "Acme.Search", name = "Search", icon = "mdiMagnify", color = "#3498db")]
#[tool(name = "search", description = "Search for information in the knowledge base")]
pub struct SearchNode {
    #[serde(flatten)]
    pub node: NodeBase,

    #[input(title = "Query", var_type = "string", scope = "Message", name = "query", description = "Search query string")]
    pub in_query: InVariable<String>,

    #[output(title = "Results", var_type = "array", scope = "Message", name = "results")]
    pub out_results: OutVariable<Vec<serde_json::Value>>,
}
```

The tool interceptor automatically handles AI tool requests and responses.

---

## 14. Repository Management

### 14.1 Building Packages

```bash
# Build package
roboctl package build

# Build for specific platforms
roboctl package build --arch windows/amd64
roboctl package build --arch darwin/arm64
roboctl package build --arch linux/amd64
```

### 14.2 Repository Management

Create and serve package repositories:

```bash
# Generate repository index from .tgz files
roboctl repo index

# Merge with existing index (preserve previous packages)
roboctl repo index --merge

# Serve repository locally for testing
roboctl repo serve --port 8080

# Serve on custom IP and port
roboctl repo serve --ip 0.0.0.0 --port 3000
```

---

## 15. Complete Example

Here's a complete example of a simple HTTP request node.

**First, the `config.json`** (defines the namespace that all node IDs must use):
```json
{
  "name": "HTTP Utilities",
  "namespace": "Acme",
  "version": "1.0.0",
  "language": "Rust",
  "platforms": ["linux", "windows", "darwin"]
}
```

**The node implementation** (note: node ID starts with `Acme.` to match the namespace):
```rust
// src/v1/http_request.rs
use robomotion::prelude::*;
use robomotion::message::Context;
use serde_json::Value;

#[derive(Node, Default)]
// Node ID "Acme.HttpRequest" matches namespace "Acme" from config.json
#[node(id = "Acme.HttpRequest", name = "HTTP Request", icon = "mdiWeb", color = "#3498db")]
pub struct HttpRequest {
    #[serde(flatten)]
    pub node: NodeBase,

    #[input(title = "URL", var_type = "string", scope = "Message", name = "url")]
    pub in_url: InVariable<String>,

    #[input(title = "Method", var_type = "string", scope = "Custom", name = "GET")]
    pub in_method: InVariable<String>,

    #[output(title = "Response", var_type = "object", scope = "Message", name = "response")]
    pub out_response: OutVariable<Value>,

    #[output(title = "Status Code", var_type = "int", scope = "Message", name = "statusCode")]
    pub out_status: OutVariable<i64>,
}

#[async_trait]
impl MessageHandler for HttpRequest {
    async fn on_create(&mut self) -> Result<()> {
        tracing::info!("HttpRequest node created");
        Ok(())
    }

    async fn on_message(&mut self, ctx: &mut Context) -> Result<()> {
        let url = self.in_url.get(ctx).await?;
        let method = self.in_method.get(ctx).await?;

        let client = reqwest::Client::new();
        let response = match method.to_uppercase().as_str() {
            "GET" => client.get(&url).send().await?,
            "POST" => client.post(&url).send().await?,
            _ => return Err(RobomotionError::Variable(format!("Unknown method: {}", method))),
        };

        let status = response.status().as_u16() as i64;
        let body: Value = response.json().await.unwrap_or(Value::Null);

        self.out_status.set(ctx, status).await?;
        self.out_response.set(ctx, body).await?;

        Ok(())
    }

    async fn on_close(&mut self) -> Result<()> {
        tracing::info!("HttpRequest node closed");
        Ok(())
    }
}
```

```rust
// src/v1/mod.rs
mod http_request;
pub use http_request::HttpRequest;
```

```rust
// src/main.rs
use robomotion::prelude::*;

mod v1;

#[tokio::main]
async fn main() {
    register_node::<v1::HttpRequest>();
    start().await;
}
```

---

## 16. Common Issues & Troubleshooting

### 16.1 Build and Runtime Issues

| Symptom | Explanation | Solution |
|---------|-------------|----------|
| `timeout: plugin listener is nil` | Binary launched but never called `start()` | Add `start().await` call in your `main.rs` |
| "node handler not found" | Node not registered or ID mismatch | Check node registration in `main.rs` and verify node ID matches Designer |
| "plugin already attached" error | Multiple plugin instances running | Stop previous instances, check running processes |
| Empty pspec file | Missing derive macro on Node | Add `#[derive(Node)]` to your struct |
| Package not updating | Robot using cached version | Clear package cache or increment version in `config.json` |
| Node not appearing in Designer | Node ID doesn't match namespace | Ensure node ID starts with namespace from `config.json` (e.g., if namespace is `Acme.Weather`, node ID must be `Acme.Weather.NodeName`) |

### 16.2 Variable and Scope Issues

| Symptom | Explanation | Solution |
|---------|-------------|----------|
| Variables always empty | Incorrect scope configuration | Check `scope` attribute in input/output macros |
| Input not appearing in Designer | Missing input attribute | Add `#[input(...)]` attribute to field |
| Output not being set | Forgot to call `.set()` | Call `self.out_field.set(ctx, value).await?` |

### 16.3 Async Issues

| Symptom | Explanation | Solution |
|---------|-------------|----------|
| "future cannot be sent between threads" | Non-Send type held across await | Use `tokio::sync` types instead of `std::sync` |
| "async trait methods not supported" | Missing async_trait | Add `#[async_trait]` to impl block |

---

## 17. Dependencies

Typical `Cargo.toml` for a Robomotion package:

```toml
[package]
name = "my-package"
version = "0.1.0"
edition = "2021"

[dependencies]
robomotion = { git = "https://github.com/robomotionio/robomotion-rust" }
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
async-trait = "0.1"
tracing = "0.1"

# Add other dependencies as needed
reqwest = { version = "0.12", features = ["json"] }
```

---

## 18. Best Practices

1. **Always handle errors gracefully** – Use `?` for propagation and provide meaningful error messages.

2. **Use appropriate types** – Use `serde_json::Value` for dynamic JSON, concrete types when structure is known.

3. **Log important events** – Use `tracing` macros (`info!`, `debug!`, `error!`) for debugging.

4. **Clean up resources** – Implement `on_close` to release connections, files, etc.

5. **Test locally first** – Use attach mode (`-a`) for rapid iteration before packaging.

6. **Version your nodes** – Use `v1/`, `v2/` modules to maintain backward compatibility.

7. **Document your nodes** – Use `description` attributes to explain what each field does.

8. **Follow naming conventions** – Consistent `in_`, `out_`, `opt_` prefixes help maintainability.
