Custom Actions
Extend job workflows with reusable YAML-defined, file-based, and built-in actions
Custom actions turn repeating logic into reusable building blocks referenced with action: my-action inside a workflow step.
from praisonaiagents import Agent
agent = Agent(name="workflow-agent", instructions="Run custom build actions.")
agent.start("Run the greet action then bump the version.")The user triggers a step; the resolver locates the matching action and runs it.
Quick Start
- Run actions from an agent
python from praisonaiagents import Agent agent = Agent(name="workflow-agent", instructions="Run custom build actions.") agent.start("Run the greet action then bump the version.") - Reference an action in a workflow step
yaml steps: - name: Say hello action: greetThat single line triggers a 3-tier resolution chain to find and run the action.
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.
How Actions Resolve
The user triggers a workflow step; the resolver finds the YAML, file, or built-in action and runs it.
| Priority | Type | Where it lives |
|---|---|---|
| 1st | YAML-defined | actions: block in the workflow file |
| 2nd | File-based | actions/my_action.py or .praison/actions/my_action.py |
| 3rd | Built-in | Shipped 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.
How It Works
The user runs a workflow; each step’s action: name is resolved through the 3-tier chain, executed, and its result returned to the step.
YAML-Defined Actions
Self-contained in the workflow file — no external files needed. Define them in the top-level actions: block.
- Define and Run
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: timestampbash praisonai workflow run yaml-actions-demo.yaml
YAML Action Types
Each action defines one key — the same types available in regular steps:
| Key | What 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.pyThe 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
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
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:
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-inforesolves toactions/system_info.py. Dashes in the action name become underscores in the filename.
Built-in Actions
Shipped with PraisonAI, no files needed:
| Action | Description |
|---|---|
bump-version | Bump version in pyproject.toml (patch, minor, or major) |
- name: Bump version
action: bump-version
file: pyproject.toml
strategy: patchFlags --major and --minor override the strategy at runtime:
praisonai workflow run release.yaml --majorPassing 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 keydef 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 priorityBest Practices
Use YAML actions for workflow-specific logic
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.
Use file-based actions for shared logic
If multiple workflows need the same action, put it in actions/ (per-project) or .praison/actions/ (project-level shared).
Always return the correct dict shape
File-based actions must return {"ok": True, "output": "..."} or {"ok": False, "error": "..."}. The workflow executor checks ok to determine pass/fail.
Use –dry-run to preview
Dry run shows all steps and their action names without executing:
praisonai workflow run my-workflow.yaml --dry-run