# Quick Reference: Action Output Format and Schema

**Last Updated:** 2026-02-07  
**Status:** Current standard for all actions

## TL;DR

- ✅ **DO:** Set `output_format` to "text", "json", or "yaml"
- ✅ **DO:** Define `output_schema` for structured outputs (json/yaml only)
- ❌ **DON'T:** Include stdout/stderr/exit_code in output schema (captured automatically)
- 💡 **Output schema** describes the shape of structured data sent to stdout

## Output Format Field

All actions must specify an `output_format` field in their YAML definition:

```yaml
name: my_action
ref: mypack.my_action
runner_type: shell
entry_point: my_action.sh

# Output format: text, json, or yaml
output_format: text  # or json, or yaml
```

### Supported Formats

| Format | Description | Worker Behavior | Use Case |
|--------|-------------|-----------------|----------|
| `text` | Plain text output | Stored as-is in execution result | Simple messages, logs, unstructured data |
| `json` | JSON structured data | Parsed into JSONB field | APIs, structured results, complex data |
| `yaml` | YAML structured data | Parsed into JSONB field | Configuration, human-readable structured data |

## Output Schema

The `output_schema` field describes the **shape of structured data** written to stdout:

- **Only applicable** for `output_format: json` or `output_format: yaml`
- **Not needed** for `output_format: text` (no parsing occurs)
- **Should NOT include** execution metadata (stdout/stderr/exit_code)

### Text Output Actions

For actions that output plain text, omit the output schema:

```yaml
name: echo
ref: core.echo
runner_type: shell
entry_point: echo.sh

# Output format: text (no structured data parsing)
output_format: text

parameters:
  type: object
  properties:
    message:
      type: string

# Output schema: not applicable for text output format
# The action outputs plain text to stdout
```

**Action script:**
```bash
#!/bin/bash
INPUT=$(cat)
MESSAGE=$(echo "$INPUT" | jq -r '.message // ""')
echo "$MESSAGE"  # Plain text to stdout
```

### JSON Output Actions

For actions that output JSON, define the schema:

```yaml
name: http_request
ref: core.http_request
runner_type: python
entry_point: http_request.py

# Output format: json (structured data parsing enabled)
output_format: json

parameters:
  type: object
  properties:
    url:
      type: string
      required: true

# Output schema: describes the JSON structure written to stdout
# Note: stdout/stderr/exit_code are captured automatically by the execution system
output_schema:
  type: object
  properties:
    status_code:
      type: integer
      description: "HTTP status code"
    body:
      type: string
      description: "Response body as text"
    success:
      type: boolean
      description: "Whether the request was successful (2xx status)"
```

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

def main():
    params = json.loads(sys.stdin.read() or '{}')
    
    # Perform HTTP request logic
    result = {
        "status_code": 200,
        "body": "Response body",
        "success": True
    }
    
    # Output JSON to stdout (worker will parse and store in execution.result)
    print(json.dumps(result, indent=2))

if __name__ == "__main__":
    main()
```

### YAML Output Actions

For actions that output YAML:

```yaml
name: get_config
ref: mypack.get_config
runner_type: shell
entry_point: get_config.sh

# Output format: yaml (structured data parsing enabled)
output_format: yaml

# Output schema: describes the YAML structure written to stdout
output_schema:
  type: object
  properties:
    server:
      type: object
      properties:
        host:
          type: string
        port:
          type: integer
    database:
      type: object
      properties:
        url:
          type: string
```

**Action script:**
```bash
#!/bin/bash
cat <<EOF
server:
  host: localhost
  port: 8080
database:
  url: postgresql://localhost/db
EOF
```

## Execution Metadata (Automatic)

The following metadata is **automatically captured** by the worker for every execution:

| Field | Type | Description | Source |
|-------|------|-------------|--------|
| `stdout` | string | Standard output from action | Captured by worker |
| `stderr` | string | Standard error output | Captured by worker, written to log file |
| `exit_code` | integer | Process exit code | Captured by worker |
| `duration_ms` | integer | Execution duration | Calculated by worker |

**Do NOT include these in your output schema** - they are execution system concerns, not action output concerns.

## Worker Behavior

### Text Format
```
Action writes to stdout: "Hello, World!"
↓
Worker captures stdout as-is
↓
Execution.result = null (no parsing)
Execution.stdout = "Hello, World!"
Execution.exit_code = 0
```

### JSON Format
```
Action writes to stdout: {"status": "success", "count": 42}
↓
Worker parses JSON
↓
Execution.result = {"count": 42, "message": "done"} (JSONB)
Execution.stdout = '{"count": 42, "message": "done"}' (raw)
Execution.exit_code = 0
```

### YAML Format
```
Action writes to stdout: 
  status: success
  count: 42
↓
Worker parses YAML to JSON
↓
Execution.result = {"count": 42, "message": "done"} (JSONB)
Execution.stdout = "count: 42\nmessage: done\n" (raw)
Execution.exit_code = 0
```

## Error Handling

### Stderr Usage

- **Purpose:** Diagnostic messages, warnings, errors
- **Storage:** Written to execution log file (not inline with result)
- **Visibility:** Available via execution logs API endpoint
- **Best Practice:** Use stderr for error messages, not stdout

**Example:**
```bash
#!/bin/bash
if [ -z "$URL" ]; then
    echo "ERROR: URL parameter is required" >&2  # stderr
    exit 1
fi

# Normal output to stdout
echo "Success"
```

### Exit Codes

- **0:** Success
- **Non-zero:** Failure
- **Captured automatically:** Worker records exit code in execution record
- **Don't output in JSON:** Exit code is metadata, not result data

## Pattern Examples

### Example 1: Simple Text Action

```yaml
# echo.yaml
name: echo
output_format: text
parameters:
  properties:
    message:
      type: string
```

```bash
# echo.sh
#!/bin/bash
INPUT=$(cat)
MESSAGE=$(echo "$INPUT" | jq -r '.message // ""')
echo "$MESSAGE"
```

### Example 2: Structured JSON Action

```yaml
# validate_json.yaml
name: validate_json
output_format: json
parameters:
  properties:
    json_data:
      type: string
output_schema:
  type: object
  properties:
    valid:
      type: boolean
    errors:
      type: array
      items:
        type: string
```

```python
# validate_json.py
#!/usr/bin/env python3
import json
import sys

def main():
    params = json.loads(sys.stdin.read() or '{}')
    json_data = params.get('json_data', '')
    
    errors = []
    valid = False
    
    try:
        json.loads(json_data)
        valid = True
    except json.JSONDecodeError as e:
        errors.append(str(e))
    
    result = {"valid": valid, "errors": errors}
    
    # Output JSON to stdout
    print(json.dumps(result))

if __name__ == "__main__":
    main()
```

### Example 3: API Wrapper with JSON Output

```yaml
# github_pr_info.yaml
name: github_pr_info
output_format: json
parameters:
  properties:
    repo:
      type: string
      required: true
    pr_number:
      type: integer
      required: true
output_schema:
  type: object
  properties:
    title:
      type: string
    state:
      type: string
      enum: [open, closed, merged]
    author:
      type: string
    created_at:
      type: string
      format: date-time
```

## Migration from Old Pattern

### Before (Incorrect)

```yaml
# DON'T DO THIS - includes execution metadata
output_schema:
  type: object
  properties:
    stdout:       # ❌ Execution metadata
      type: string
    stderr:       # ❌ Execution metadata
      type: string
    exit_code:    # ❌ Execution metadata
      type: integer
    result:
      type: object  # ❌ Actual result unnecessarily nested
```

### After (Correct)

```yaml
# DO THIS - only describe the actual data structure your action outputs
output_format: json
output_schema:
  type: object
  properties:
    count:
      type: integer
    items:
      type: array
      items:
        type: string
    # No stdout/stderr/exit_code - those are captured automatically
```

## Best Practices

1. **Choose the right format:**
   - Use `text` for simple messages, logs, or unstructured output
   - Use `json` for structured data, API responses, complex results
   - Use `yaml` for human-readable configuration or structured output

2. **Keep output schema clean:**
   - Only describe the actual data structure
   - Don't include execution metadata
   - Don't nest result under a "result" or "data" key unless semantic

3. **Use stderr for diagnostics:**
   - Error messages go to stderr, not stdout
   - Debugging output goes to stderr
   - Normal results go to stdout

4. **Exit codes matter:**
   - 0 = success (even if result indicates failure semantically)
   - Non-zero = execution failure (script error, crash, etc.)
   - Don't output exit code in JSON - it's captured automatically

5. **Validate your schema:**
   - Ensure output schema matches actual JSON/YAML structure
   - Test with actual action outputs
   - Use JSON Schema validation tools

6. **Document optional fields:**
   - Mark fields that may not always be present
   - Provide descriptions for all fields
   - Include examples in action documentation

## Testing

### Test Text Output
```bash
echo '{"message": "test"}' | ./action.sh
# Verify: Plain text output, no JSON structure
```

### Test JSON Output
```bash
echo '{"url": "https://example.com"}' | ./action.py | jq .
# Verify: Valid JSON, matches schema
```

### Test Error Handling
```bash
echo '{}' | ./action.sh 2>&1
# Verify: Errors to stderr, proper exit code
```

### Test Schema Compliance
```bash
OUTPUT=$(echo '{"param": "value"}' | ./action.py)
echo "$OUTPUT" | jq -e '.status and .data' > /dev/null
# Verify: Output has required fields from schema
```

## Common Pitfalls

### ❌ Pitfall 1: Including Execution Metadata
```yaml
# WRONG
output_schema:
  properties:
    exit_code:      # ❌ Automatic
      type: integer
    stdout:         # ❌ Automatic
      type: string
```

### ❌ Pitfall 2: Missing output_format
```yaml
# WRONG - no output_format specified
name: my_action
output_schema:  # How should this be parsed?
  type: object
```

### ❌ Pitfall 3: Text Format with Schema
```yaml
# WRONG - text format doesn't need schema
output_format: text
output_schema:  # ❌ Ignored for text format
  type: object
```

### ❌ Pitfall 4: Unnecessary Nesting
```bash
# WRONG - unnecessary "result" wrapper
echo '{"result": {"count": 5, "name": "test"}}'  # ❌

# RIGHT - output the data structure directly
echo '{"count": 5, "name": "test"}'  # ✅
```

## References

- [Action Parameter Handling](./QUICKREF-action-parameters.md) - Stdin-based parameter delivery
- [Core Pack Actions](../packs/core/actions/README.md) - Reference implementations
- [Worker Service Architecture](./architecture/worker-service.md) - How worker processes actions

## See Also

- Execution API endpoints (for retrieving results)
- Workflow parameter mapping (for using action outputs)
- Logging configuration (for stderr handling)