# Action Development Guide

**Complete guide for developing actions in Attune**

## Table of Contents

1. [Introduction](#introduction)
2. [Action Anatomy](#action-anatomy)
3. [Parameter Configuration](#parameter-configuration)
4. [Output Configuration](#output-configuration)
5. [Standard Environment Variables](#standard-environment-variables)
6. [Runtime Configuration](#runtime-configuration)
7. [Complete Examples](#complete-examples)
8. [Best Practices](#best-practices)
9. [Troubleshooting](#troubleshooting)

---

## Introduction

Actions are the fundamental building blocks of automation in Attune. Each action is a script or program that performs a specific task, receives parameters, and returns results. This guide covers everything you need to know to write effective actions.

### What You'll Learn

- How to configure parameter delivery methods (stdin, file)
- How to format parameters (JSON, YAML, dotenv)
- How to specify output formats for structured data
- What environment variables are available to your actions
- How to write actions for different runtimes (Shell, Python, Node.js)
- Best practices for security and reliability

---

## Action Anatomy

Every action consists of two files:

1. **Metadata file** (`actions/<action_name>.yaml`) - Describes the action
2. **Implementation file** (`actions/<action_name>.<ext>`) - Executes the logic

### Metadata File Structure

```yaml
ref: mypack.my_action
label: "My Action"
description: "Action description"
enabled: true

# Runtime configuration
runner_type: shell              # Runtime to use (shell, python, nodejs, etc.)
entry_point: my_action.sh       # Script to execute

# Parameter configuration (how parameters are delivered)
parameter_delivery: stdin       # Options: stdin (default), file
parameter_format: json          # Options: json (default), yaml, dotenv

# Output configuration (how output is parsed)
output_format: json             # Options: text (default), json, yaml, jsonl

# Parameter schema (JSON Schema format)
# Note: 'required' is an array of required property names
# If no properties are required, omit the 'required' field entirely
parameters:
  type: object
  properties:
    message:
      type: string
      description: "Message to process"
    count:
      type: integer
      description: "Number of times to repeat"
      default: 1
  required:
    - message

# Output schema (documents expected JSON structure)
output_schema:
  type: object
  properties:
    result:
      type: string
      description: "Processed result"
    success:
      type: boolean
      description: "Whether operation succeeded"

tags:
  - utility
```

---

## Parameter Configuration

Parameters are the inputs to your action. Attune provides flexible configuration for how parameters are delivered and formatted.

### Parameter Delivery Methods

#### 1. **Stdin Delivery** (Recommended, Default)

Parameters are passed via standard input. This is the **most secure method** as parameters don't appear in process listings.

```yaml
parameter_delivery: stdin
parameter_format: json
```

**Reading stdin parameters:**

The worker writes a single document to stdin containing all parameters (including secrets merged in), followed by a newline, then closes stdin:

```
<formatted_parameters>\n
```

- Parameters and secrets are merged into a single document
- Secrets are included as top-level keys in the parameters object
- The action reads until EOF (stdin is closed after delivery)

#### 2. **File Delivery**

Parameters are written to a temporary file with restrictive permissions (owner read-only, 0400).

```yaml
parameter_delivery: file
parameter_format: yaml
```

**Reading file parameters:**

The file path is provided in the `ATTUNE_PARAMETER_FILE` environment variable:

```bash
# Shell example
PARAM_FILE="$ATTUNE_PARAMETER_FILE"
params=$(cat "$PARAM_FILE")
```

### Parameter Formats

#### 1. **JSON Format** (Default)

Standard JSON object format.

```yaml
parameter_format: json
```

**Example output:**
```json
{
  "message": "Hello, World!",
  "count": 42,
  "enabled": true
}
```

**Reading JSON (Shell with jq):**
```bash
#!/bin/bash
set -e

# Read JSON from stdin
read -r -d '' PARAMS_JSON || true
MESSAGE=$(echo "$PARAMS_JSON" | jq -r '.message')
COUNT=$(echo "$PARAMS_JSON" | jq -r '.count // 1')
```

**Reading JSON (Python):**
```python
#!/usr/bin/env python3
import json
import sys

# Read all parameters from stdin (secrets are merged in)
content = sys.stdin.read().strip()
params = json.loads(content) if content else {}

message = params.get('message', '')
count = params.get('count', 1)
```

#### 2. **YAML Format**

YAML format, useful for complex nested structures.

```yaml
parameter_format: yaml
```

**Example output:**
```yaml
message: Hello, World!
count: 42
enabled: true
nested:
  key: value
```

**Reading YAML (Python):**
```python
#!/usr/bin/env python3
import sys
import yaml

# Read all parameters from stdin (secrets are merged in)
content = sys.stdin.read().strip()
params = yaml.safe_load(content) if content else {}

message = params.get('message', '')
```

#### 3. **Dotenv Format**

Simple key-value pairs, one per line. Best for shell scripts with simple parameters.

```yaml
parameter_format: dotenv
```

**Example output:**
```bash
message='Hello, World!'
count='42'
enabled='true'
```

**Reading dotenv (Shell):**
```bash
#!/bin/sh
set -e

# Initialize variables
message=""
count=""

# Read until delimiter
while IFS= read -r line; do
    case "$line" in
        message=*) 
            message="${line#message=}"
            # Remove quotes
            message="${message#[\"']}"
            message="${message%[\"']}"
            ;;
        count=*)
            count="${line#count=}"
            count="${count#[\"']}"
            count="${count%[\"']}"
            ;;
    esac
done

echo "Message: $message"
echo "Count: $count"
```

**Example with no required parameters:**
```yaml
# All parameters are optional
parameters:
  type: object
  properties:
    message:
      type: string
      description: "Optional message to log"
      default: "Hello"
    verbose:
      type: boolean
      description: "Enable verbose logging"
      default: false
  # Note: No 'required' field - all parameters are optional
```

### Security Considerations

- **Never use environment variables for secrets** - Environment variables are visible in process listings (`ps aux`)
- **Always use stdin or file delivery for sensitive data** - These methods are not visible to other processes
- **Secrets are always passed via stdin** - Even if parameters use file delivery, secrets come through stdin after the delimiter
- **Parameter files have restrictive permissions** - 0400 (owner read-only)

---

## Output Configuration

Configure how your action's output is parsed and stored in the execution result.

### Output Formats

#### 1. **Text Format** (Default)

No parsing - output is captured as plain text in `execution.stdout`.

```yaml
output_format: text
```

**Use case:** Simple actions that output messages, logs, or unstructured text.

**Example:**
```bash
#!/bin/bash
echo "Task completed successfully"
echo "Processed 42 items"
exit 0
```

**Result:**
- `execution.stdout`: Full text output
- `execution.result`: `null` (no parsing)

#### 2. **JSON Format**

Parses the **last line** of stdout as JSON and stores it in `execution.result`.

```yaml
output_format: json
```

**Use case:** Actions that return structured data, API responses, or computed results.

**Example:**
```bash
#!/bin/bash
# Your action logic here
curl -s https://api.example.com/data

# Output JSON as last line (curl already outputs JSON)
# The worker will parse this into execution.result
exit 0
```

**Example (manual JSON):**
```python
#!/usr/bin/env python3
import json

result = {
    "status": "success",
    "items_processed": 42,
    "duration_ms": 1234
}

# Output JSON - will be parsed into execution.result
print(json.dumps(result, indent=2))
```

**Result:**
- `execution.stdout`: Full output including JSON
- `execution.result`: Parsed JSON object from last line

#### 3. **YAML Format**

Parses the entire stdout as YAML.

```yaml
output_format: yaml
```

**Use case:** Actions that generate YAML configuration or reports.

**Example:**
```python
#!/usr/bin/env python3
import yaml

result = {
    "status": "success",
    "config": {
        "enabled": True,
        "timeout": 30
    }
}

print(yaml.dump(result))
```

#### 4. **JSONL Format** (JSON Lines)

Parses each line of stdout as a separate JSON object and collects them into an array.

```yaml
output_format: jsonl
```

**Use case:** Streaming results, processing multiple items, progress updates.

**Example:**
```python
#!/usr/bin/env python3
import json

# Process items and output each as JSON
for i in range(5):
    item = {"id": i, "status": "processed"}
    print(json.dumps(item))  # Each line is valid JSON
```

**Result:**
- `execution.result`: Array of parsed JSON objects

---

## Standard Environment Variables

The worker provides these environment variables to **all** action executions:

### Core Variables (Always Present)

| Variable | Description | Example |
|----------|-------------|---------|
| `ATTUNE_EXEC_ID` | Execution database ID | `12345` |
| `ATTUNE_ACTION` | Action reference (pack.action) | `core.echo` |
| `ATTUNE_API_URL` | Attune API base URL | `http://api:8080` |
| `ATTUNE_API_TOKEN` | Execution-scoped API token | `eyJ0eXAi...` |

### Contextual Variables (When Applicable)

| Variable | Description | Present When |
|----------|-------------|--------------|
| `ATTUNE_RULE` | Rule reference that triggered execution | Execution triggered by rule |
| `ATTUNE_TRIGGER` | Trigger reference that fired | Execution triggered by event |
| `ATTUNE_CONTEXT_*` | Custom context data | Context provided in execution config |

### Parameter Delivery Variables

| Variable | Description | Present When |
|----------|-------------|--------------|
| `ATTUNE_PARAMETER_DELIVERY` | Delivery method used | Always (`stdin` or `file`) |
| `ATTUNE_PARAMETER_FORMAT` | Format used | Always (`json`, `yaml`, or `dotenv`) |
| `ATTUNE_PARAMETER_FILE` | Path to parameter file | `parameter_delivery: file` |

### Using Environment Variables

**Shell:**
```bash
#!/bin/bash
set -e

echo "Execution ID: $ATTUNE_EXEC_ID"
echo "Action: $ATTUNE_ACTION"
echo "API URL: $ATTUNE_API_URL"

# Check if triggered by rule
if [ -n "$ATTUNE_RULE" ]; then
    echo "Triggered by rule: $ATTUNE_RULE"
    echo "Trigger: $ATTUNE_TRIGGER"
fi

# Use API token for authenticated requests
curl -H "Authorization: Bearer $ATTUNE_API_TOKEN" \
     "$ATTUNE_API_URL/api/executions/$ATTUNE_EXEC_ID"
```

**Python:**
```python
#!/usr/bin/env python3
import os
import requests

exec_id = os.environ['ATTUNE_EXEC_ID']
action_ref = os.environ['ATTUNE_ACTION']
api_url = os.environ['ATTUNE_API_URL']
api_token = os.environ['ATTUNE_API_TOKEN']

print(f"Execution {exec_id} running action {action_ref}")

# Make authenticated API request
headers = {'Authorization': f'Bearer {api_token}'}
response = requests.get(f"{api_url}/api/executions/{exec_id}", headers=headers)
print(response.json())
```

### Custom Environment Variables

You can also set custom environment variables per execution via the `env_vars` field:

```json
{
  "action_ref": "core.my_action",
  "parameters": {
    "message": "Hello"
  },
  "env_vars": {
    "DEBUG": "true",
    "LOG_LEVEL": "verbose"
  }
}
```

**Note:** These are separate from parameters and passed as actual environment variables.

---

## Runtime Configuration

Configure which runtime executes your action.

### Available Runtimes

| Runtime | `runner_type` | Description | Entry Point |
|---------|---------------|-------------|-------------|
| Shell | `shell` | POSIX shell scripts | Script file (`.sh`) |
| Python | `python` | Python 3.x scripts | Script file (`.py`) |
| Node.js | `nodejs` | JavaScript/Node.js | Script file (`.js`) |
| Native | `native` | Compiled binaries | Binary file |
| Local | `local` | Local system commands | Command name |

### Shell Runtime

Execute shell scripts using `/bin/sh` (or configurable shell).

```yaml
runner_type: shell
entry_point: my_script.sh
```

**Script requirements:**
- Must be executable or have a shebang (`#!/bin/sh`)
- Exit code 0 indicates success
- Output to stdout for results
- Errors to stderr

**Example:**
```bash
#!/bin/sh
set -e  # Exit on error

# Read parameters from stdin
content=$(cat)
params=$(echo "$content" | head -n 1)

# Process
echo "Processing: $params"

# Output result
echo '{"status": "success"}'
exit 0
```

### Python Runtime

Execute Python scripts with automatic virtual environment management.

```yaml
runner_type: python
entry_point: my_script.py
```

**Features:**
- Automatic virtual environment creation
- Dependency installation from `requirements.txt`
- Python 3.x support
- Access to all standard libraries

**Example:**
```python
#!/usr/bin/env python3
import json
import sys

def main():
    # Read parameters (secrets are merged into the same document)
    content = sys.stdin.read().strip()
    params = json.loads(content) if content else {}
    
    # Process
    message = params.get('message', '')
    result = message.upper()
    
    # Output
    print(json.dumps({
        'result': result,
        'success': True
    }))
    return 0

if __name__ == '__main__':
    sys.exit(main())
```

**Dependencies (optional `requirements.txt` in action directory):**
```txt
requests>=2.28.0
pyyaml>=6.0
```

### Node.js Runtime

Execute JavaScript with Node.js.

```yaml
runner_type: nodejs
entry_point: my_script.js
```

**Example:**
```javascript
#!/usr/bin/env node
const readline = require('readline');

async function main() {
    // Read stdin
    const rl = readline.createInterface({
        input: process.stdin,
        output: process.stdout,
        terminal: false
    });

    let input = '';
    for await (const line of rl) {
        input += line;
    }

    const params = JSON.parse(input || '{}');
    
    // Process
    const result = {
        message: params.message.toUpperCase(),
        success: true
    };

    // Output
    console.log(JSON.stringify(result, null, 2));
}

main().catch(err => {
    console.error(err);
    process.exit(1);
});
```

### Native Runtime

Execute compiled binaries (sensors, performance-critical actions).

```yaml
runner_type: native
entry_point: my_binary
```

**Use case:** Compiled sensors, performance-critical operations.

**Requirements:**
- Binary must be executable
- Built for target architecture (see `scripts/build-pack-binaries.sh`)
- Follow same stdin/stdout conventions

---

## Complete Examples

### Example 1: Simple Text Action

**Metadata (`actions/greet.yaml`):**
```yaml
ref: mypack.greet
label: "Greet User"
description: "Greet a user by name"
runner_type: shell
entry_point: greet.sh
parameter_delivery: stdin
parameter_format: json
output_format: text

parameters:
  type: object
  properties:
    name:
      type: string
      description: "Name to greet"
    formal:
      type: boolean
      description: "Use formal greeting"
      default: false
  required:
    - name
```

**Implementation (`actions/greet.sh`):**
```bash
#!/bin/bash
set -e

# Read JSON parameters
read -r -d '' PARAMS_JSON || true
NAME=$(echo "$PARAMS_JSON" | jq -r '.name')
FORMAL=$(echo "$PARAMS_JSON" | jq -r '.formal // false')

# Generate greeting
if [ "$FORMAL" = "true" ]; then
    echo "Good day, $NAME. It is a pleasure to meet you."
else
    echo "Hey $NAME! What's up?"
fi

exit 0
```

### Example 2: HTTP API Action with JSON Output

**Metadata (`actions/fetch_user.yaml`):**
```yaml
ref: mypack.fetch_user
label: "Fetch User"
description: "Fetch user data from API"
runner_type: shell
entry_point: fetch_user.sh
parameter_delivery: stdin
parameter_format: json
output_format: json

parameters:
  type: object
  properties:
    user_id:
      type: integer
      description: "User ID to fetch"
    include_posts:
      type: boolean
      description: "Include user posts"
      default: false
  required:
    - user_id

output_schema:
  type: object
  properties:
    user:
      type: object
      description: "User data"
    posts:
      type: array
      description: "User posts (if requested)"
    success:
      type: boolean
```

**Implementation (`actions/fetch_user.sh`):**
```bash
#!/bin/bash
set -e

# Read parameters
read -r -d '' PARAMS_JSON || true
USER_ID=$(echo "$PARAMS_JSON" | jq -r '.user_id')
INCLUDE_POSTS=$(echo "$PARAMS_JSON" | jq -r '.include_posts // false')

# Fetch user
USER_DATA=$(curl -s "https://jsonplaceholder.typicode.com/users/$USER_ID")

# Build result
RESULT="{\"user\": $USER_DATA"

# Optionally fetch posts
if [ "$INCLUDE_POSTS" = "true" ]; then
    POSTS=$(curl -s "https://jsonplaceholder.typicode.com/users/$USER_ID/posts")
    RESULT="$RESULT, \"posts\": $POSTS"
fi

RESULT="$RESULT, \"success\": true}"

# Output JSON (will be parsed into execution.result)
echo "$RESULT"
exit 0
```

### Example 3: Python Action with Secrets

**Metadata (`actions/send_email.yaml`):**
```yaml
ref: mypack.send_email
label: "Send Email"
description: "Send email via SMTP"
runner_type: python
entry_point: send_email.py
parameter_delivery: stdin
parameter_format: json
output_format: json

parameters:
  type: object
  properties:
    to:
      type: string
      description: "Recipient email"
    subject:
      type: string
      description: "Email subject"
    body:
      type: string
      description: "Email body"
    smtp_password:
      type: string
      description: "SMTP password"
      secret: true
  required:
    - to
    - subject
    - body
```

**Implementation (`actions/send_email.py`):**
```python
#!/usr/bin/env python3
import json
import sys
import smtplib
from email.mime.text import MIMEText

def read_stdin_params():
    """Read parameters from stdin. Secrets are already merged into the parameters."""
    content = sys.stdin.read().strip()
    return json.loads(content) if content else {}

def main():
    try:
        params = read_stdin_params()
        
        # Extract parameters
        to = params['to']
        subject = params['subject']
        body = params['body']
        smtp_password = params.get('smtp_password', '')
        
        # Create message
        msg = MIMEText(body)
        msg['Subject'] = subject
        msg['From'] = 'noreply@example.com'
        msg['To'] = to
        
        # Send (example - configure for your SMTP server)
        # with smtplib.SMTP('smtp.example.com', 587) as server:
        #     server.starttls()
        #     server.login('user', smtp_password)
        #     server.send_message(msg)
        
        # Simulate success
        result = {
            'success': True,
            'to': to,
            'subject': subject,
            'message': 'Email sent successfully'
        }
        
        print(json.dumps(result, indent=2))
        return 0
        
    except Exception as e:
        result = {
            'success': False,
            'error': str(e)
        }
        print(json.dumps(result, indent=2))
        return 1

if __name__ == '__main__':
    sys.exit(main())
```

### Example 4: Multi-Item Processing with JSONL

**Metadata (`actions/process_items.yaml`):**
```yaml
ref: mypack.process_items
label: "Process Items"
description: "Process multiple items and stream results"
runner_type: python
entry_point: process_items.py
parameter_delivery: stdin
parameter_format: json
output_format: jsonl

parameters:
  type: object
  properties:
    items:
      type: array
      items:
        type: string
      description: "Items to process"
  required:
    - items
```

**Implementation (`actions/process_items.py`):**
```python
#!/usr/bin/env python3
import json
import sys
import time

def main():
    # Read parameters (secrets are merged into the same document)
    content = sys.stdin.read().strip()
    params = json.loads(content) if content else {}
    
    items = params.get('items', [])
    
    # Process each item and output immediately (streaming)
    for idx, item in enumerate(items):
        # Simulate processing
        time.sleep(0.1)
        
        # Output one JSON object per line
        result = {
            'index': idx,
            'item': item,
            'processed': item.upper(),
            'timestamp': time.time()
        }
        print(json.dumps(result))  # Each line is valid JSON
        sys.stdout.flush()  # Ensure immediate output
    
    return 0

if __name__ == '__main__':
    sys.exit(main())
```

### Example 5: Shell Action with Dotenv Parameters

**Metadata (`actions/backup.yaml`):**
```yaml
ref: mypack.backup
label: "Backup Files"
description: "Backup files to destination"
runner_type: shell
entry_point: backup.sh
parameter_delivery: stdin
parameter_format: dotenv
output_format: text

parameters:
  type: object
  properties:
    source:
      type: string
      description: "Source directory"
    destination:
      type: string
      description: "Backup destination"
    compress:
      type: boolean
      description: "Compress backup"
      default: true
  required:
    - source
    - destination
```

**Implementation (`actions/backup.sh`):**
```bash
#!/bin/sh
set -e

# Initialize variables
source=""
destination=""
compress="true"

# Read dotenv format from stdin
while IFS= read -r line; do
    case "$line" in
        source=*)
            source="${line#source=}"
            source="${source#[\"\']}"
            source="${source%[\"\']}"
            ;;
        destination=*)
            destination="${line#destination=}"
            destination="${destination#[\"\']}"
            destination="${destination%[\"\']}"
            ;;
        compress=*)
            compress="${line#compress=}"
            compress="${compress#[\"\']}"
            compress="${compress%[\"\']}"
            ;;
    esac
done

echo "Backing up $source to $destination"

# Perform backup
if [ "$compress" = "true" ]; then
    tar -czf "$destination/backup.tar.gz" -C "$source" .
    echo "Compressed backup created at $destination/backup.tar.gz"
else
    cp -r "$source" "$destination"
    echo "Backup copied to $destination"
fi

exit 0
```

---

## Best Practices

### Security

1. **Never log secrets** - Don't echo parameters that might contain secrets
2. **Use stdin for sensitive data** - Avoid file delivery for credentials
3. **Validate inputs** - Check parameters before using them
4. **Use API token** - Authenticate API requests with `$ATTUNE_API_TOKEN`
5. **Restrict file permissions** - Keep temporary files secure

### Reliability

1. **Exit codes matter** - Return 0 for success, non-zero for failure
2. **Handle errors gracefully** - Use `set -e` in shell scripts
3. **Provide meaningful errors** - Write error details to stderr
4. **Set timeouts** - Avoid infinite loops or hangs
5. **Clean up resources** - Remove temporary files

### Output

1. **Structure output properly** - Match your `output_format` setting
2. **Validate JSON** - Ensure JSON output is valid
3. **Use stderr for logs** - Reserve stdout for results
4. **Keep output concise** - Large outputs are truncated (10MB default)
5. **Document output schema** - Define `output_schema` in metadata

### Performance

1. **Minimize dependencies** - Fewer dependencies = faster execution
2. **Use appropriate runtime** - Shell for simple tasks, Python for complex logic
3. **Stream large results** - Use JSONL for incremental output
4. **Avoid unnecessary work** - Check prerequisites early

### Maintainability

1. **Document parameters** - Provide clear descriptions
2. **Provide examples** - Include usage examples in metadata
3. **Version your actions** - Use pack versioning
4. **Test thoroughly** - Test with various parameter combinations
5. **Handle edge cases** - Empty inputs, missing optional parameters

---

## Troubleshooting

### My action isn't receiving parameters

**Check:**
- Is `parameter_delivery` set correctly?
- Are you reading from stdin or checking `$ATTUNE_PARAMETER_FILE`?
- Are you reading stdin until EOF?

**Debug:**
```bash
# Dump stdin to stderr for debugging
cat > /tmp/debug_stdin.txt
cat /tmp/debug_stdin.txt >&2
```

### My JSON output isn't being parsed

**Check:**
- Is `output_format: json` set in metadata?
- Is the last line of stdout valid JSON?
- Are you outputting anything else to stdout?

**Debug:**
```python
import json
result = {"test": "value"}
print(json.dumps(result))  # Ensure this is last line
```

### My action times out

**Check:**
- Default timeout is 5 minutes (300 seconds)
- Is your action hanging or waiting for input?
- Are you flushing output buffers?

**Fix:**
```python
print(result)
sys.stdout.flush()  # Ensure output is written immediately
```

### Secrets aren't available

**Check:**
- Are secrets configured for the action?
- Secrets are merged into the parameters document — access them by key name just like regular parameters

**Example:**
```python
content = sys.stdin.read().strip()
params = json.loads(content) if content else {}
api_key = params.get('api_key', '')  # Secrets are regular keys
```

### Environment variables are missing

**Check:**
- Standard variables (`ATTUNE_EXEC_ID`, etc.) are always present
- Context variables (`ATTUNE_RULE`, etc.) are conditional
- Custom `env_vars` must be set in execution config

**Debug:**
```bash
env | grep ATTUNE >&2  # Print all ATTUNE_* variables to stderr
```

### Output is truncated

**Cause:** Default limit is 10MB for stdout/stderr

**Solution:**
- Reduce output size
- Use artifacts for large data
- Stream results using JSONL format

---

## Additional Resources

- [Pack Structure Documentation](pack-structure.md)
- [Worker Service Architecture](architecture/worker-service.md)
- [Secrets Management](authentication/secrets-management.md)
- [Testing Actions](packs/PACK_TESTING.md)

---

## Quick Reference Card

| Configuration | Options | Default | Description |
|--------------|---------|---------|-------------|
| `runner_type` | `shell`, `python`, `nodejs`, `native`, `local` | Required | Runtime to execute action |
| `parameter_delivery` | `stdin`, `file` | `stdin` | How parameters are delivered |
| `parameter_format` | `json`, `yaml`, `dotenv` | `json` | Format for parameter serialization |
| `output_format` | `text`, `json`, `yaml`, `jsonl` | `text` | How to parse stdout output |

### Standard Environment Variables

- `ATTUNE_EXEC_ID` - Execution database ID
- `ATTUNE_ACTION` - Action reference (pack.action)
- `ATTUNE_API_URL` - API base URL
- `ATTUNE_API_TOKEN` - Execution-scoped token
- `ATTUNE_RULE` - Rule ref (if triggered by rule)
- `ATTUNE_TRIGGER` - Trigger ref (if triggered by event)

### Exit Codes

- `0` - Success
- Non-zero - Failure (error message from stderr)

### Output Parsing

- **Text**: No parsing, captured in `execution.stdout`
- **JSON**: Last line parsed into `execution.result`
- **YAML**: Full output parsed into `execution.result`
- **JSONL**: Each line parsed, collected into array in `execution.result`
