Skip to main content
Custom actions turn repeating logic into reusable building blocks for job workflows. Define them inline in YAML, as standalone Python files, or use built-in actions — all referenced with action: my-action.
steps:
  - name: Say hello
    action: greet
That single line triggers a 3-tier resolution chain to find and run the action.

How Actions Resolve

PriorityTypeWhere it lives
1stYAML-definedactions: block in the workflow file
2ndFile-basedactions/my_action.py or .praison/actions/my_action.py
3rdBuilt-inShipped with PraisonAI (e.g., bump-version)
[!TIP] YAML-defined actions always win. This lets you override any file-based or built-in action per-workflow.

YAML-Defined Actions

Self-contained in the workflow file — no external files needed. Define them in the top-level actions: block.
yaml-actions-demo.yaml
type: job
name: yaml-actions-demo
description: Demonstrates YAML-defined custom actions

actions:
  check-python:
    run: python3 --version

  count-files:
    script: |
      import os
      cwd = os.getcwd()
      py_files = [f for f in os.listdir(cwd) if f.endswith('.py')]
      yaml_files = [f for f in os.listdir(cwd) if f.endswith('.yaml') or f.endswith('.yml')]
      result = f"Python: {len(py_files)}, YAML: {len(yaml_files)}"

  timestamp:
    script: |
      from datetime import datetime
      result = datetime.now().strftime("%Y-%m-%d %H:%M:%S")

steps:
  - name: Check Python version
    action: check-python

  - name: Count project files
    action: count-files

  - name: Get timestamp
    action: timestamp
praisonai workflow run yaml-actions-demo.yaml

YAML Action Types

Each action defines one key — the same types available in regular steps:
KeyWhat it does
run:Shell command
script:Inline Python (set result variable for output)
python:Run a Python script file

File-Based Actions

Reusable .py files that can be shared across multiple workflows.

Directory Structure

my-project/
├── deploy.yaml             ← workflow file
├── actions/                ← per-workflow actions
│   ├── system_info.py
│   └── line_count.py
└── .praison/               ← project-level actions
    └── actions/
        └── deploy_k8s.py

The Action Contract

Every action file must export a run function:
def run(step, flags, cwd):
    """
    Args:
        step:  dict — the full YAML step (access custom keys)
        flags: dict — CLI flags (e.g., {"major": True})
        cwd:   str  — workflow working directory

    Returns:
        dict with:
          {"ok": True, "output": "..."} on success
          {"ok": False, "error": "..."} on failure
    """
    return {"ok": True, "output": "Hello from my action!"}

Example: System Info Action

actions/system_info.py
import platform
import os

def run(step, flags, cwd):
    """Collect system information."""
    info_parts = [
        f"OS: {platform.system()} {platform.release()}",
        f"Python: {platform.python_version()}",
        f"Machine: {platform.machine()}",
        f"CWD: {cwd}",
    ]

    # Read custom config from the step YAML
    if step.get("show_env"):
        for var in step["show_env"].split(","):
            var = var.strip()
            info_parts.append(f"{var}: {os.environ.get(var, '(not set)')}")

    return {"ok": True, "output": " | ".join(info_parts)}

Example: Line Count Action

actions/line_count.py
from pathlib import Path

def run(step, flags, cwd):
    """Count lines in files matching a glob pattern."""
    pattern = step.get("pattern", "*.py")
    directory = step.get("directory", ".")
    target = Path(cwd) / directory

    if not target.exists():
        return {"ok": False, "error": f"Directory not found: {target}"}

    total_lines = 0
    file_count = 0

    for filepath in target.rglob(pattern):
        if filepath.is_file():
            try:
                total_lines += len(filepath.read_text().splitlines())
                file_count += 1
            except (UnicodeDecodeError, PermissionError):
                continue

    return {
        "ok": True,
        "output": f"{file_count} files, {total_lines} lines ({pattern} in {directory})",
    }

Using File-Based Actions

Reference them by name — dashes become underscores when looking up the .py file:
file-actions-demo.yaml
type: job
name: file-actions-demo
description: Demonstrates file-based custom actions

steps:
  - name: System info
    action: system-info

  - name: Count Python files
    action: line-count
    pattern: "*.py"
    directory: "."

  - name: Count YAML files
    action: line-count
    pattern: "*.yaml"
    directory: "."
praisonai workflow run file-actions-demo.yaml
[!NOTE] action: system-info resolves to actions/system_info.py. Dashes in the action name become underscores in the filename.

Built-in Actions

Shipped with PraisonAI, no files needed:
ActionDescription
bump-versionBump version in pyproject.toml (patch, minor, or major)
- name: Bump version
  action: bump-version
  file: pyproject.toml
  strategy: patch
Flags --major and --minor override the strategy at runtime:
praisonai workflow run release.yaml --major

Passing Data to Actions

Custom keys on the step are passed through to the action via the step dict:
steps:
  - name: Deploy to staging
    action: deploy
    environment: staging       # ← custom key
    region: us-east-1          # ← custom key
    skip_health_check: false   # ← custom key
actions/deploy.py
def run(step, flags, cwd):
    env = step.get("environment", "production")
    region = step.get("region", "us-west-2")
    skip_health = step.get("skip_health_check", False)

    # ... deploy logic ...
    return {"ok": True, "output": f"Deployed to {env} in {region}"}

Resolution Priority Demo

When the same action name exists in both YAML and as a file, YAML wins:
type: job

actions:
  greet:
    script: |
      result = "Hello from YAML!"    # ← this runs

steps:
  - name: Greet
    action: greet
    # Even if actions/greet.py exists, the YAML definition takes priority

Best Practices

If the action is only used in one workflow, define it inline. This keeps the workflow portable — a single file you can copy or share.
If multiple workflows need the same action, put it in actions/ (per-project) or .praison/actions/ (project-level shared).
File-based actions must return {"ok": True, "output": "..."} or {"ok": False, "error": "..."}. The workflow executor checks ok to determine pass/fail.
Dry run shows all steps and their action names without executing:
praisonai workflow run my-workflow.yaml --dry-run