Skip to main content
Self-improving skills enable agents to create, edit, and manage their own capabilities dynamically — with mutations staged for human approval before they touch disk.
from praisonaiagents import Agent

agent = Agent(
    name="Skill Builder",
    instructions="When users teach you something, save it as a skill for next time.",
    tools=["skill_manage", "skills_list", "skill_view"],
)
agent.start("Create a skill called 'weekly-summary' that summarises the week's work.")
The user teaches the agent; skill changes stay pending until a human approves them.
To trigger skill creation automatically after every task instead of having the model call skill_manage on its own, see Self-Improving Agents.
Safe-by-default since PR #2236. Every mutation (create, edit, patch, delete, write_file, remove_file) is now staged for human approval by default. The agent receives {"status": "pending", "id": "skl-…"} instead of writing to disk. Pass propose=False to restore direct-write behaviour in trusted/local contexts.

Quick Start

1

Agent proposes a skill (default behaviour)

from praisonaiagents import Agent

agent = Agent(
    name="Skill Builder",
    instructions="When users teach you something, save it as a skill for next time.",
    tools=["skill_manage", "skills_list", "skill_view"]
)

agent.start("Create a skill called 'weekly-summary' that summarises the week's work.")
# Agent calls skill_manage(action="create", name="weekly-summary", content="...")
# Tool returns: {"status": "pending", "id": "skl-a1b2c3d4"}
# Agent replies: "Proposed — pending approval skl-a1b2c3d4"
2

Human reviews and approves

from praisonaiagents import SkillManager

mgr = SkillManager()

# See what's waiting
mgr.list_pending()
# [{'id': 'skl-a1b2c3d4', 'action': 'create', 'name': 'weekly-summary', 'status': 'pending', 'created_at': '...'}]

# Apply the mutation
mgr.approve("skl-a1b2c3d4")
# {"success": True, "skill": "weekly-summary"}

# — or discard it —
mgr.reject("skl-a1b2c3d4")
3

Direct write (trusted/local contexts only)

from praisonaiagents import Agent

agent = Agent(
    name="Skill Builder",
    instructions="Save skills immediately when users teach you something.",
    tools=["skill_manage", "skills_list", "skill_view"]
)

# Bypass the approval gate — writes straight to disk
agent.start("Create a skill called 'csv-analysis' … [propose=False]")
4

Archive and recover skills

from praisonaiagents import SkillManager

mgr = SkillManager()
mgr.discover()

mgr.delete_skill("weekly-summary")          # archives by default (recoverable)
mgr.restore_skill("weekly-summary")

mgr.archive_skill("csv-analysis")
archived = mgr.list_archived_skills()       # ["csv-analysis"]
mgr.restore_skill("csv-analysis")

Approval Gate (safe-by-default)

Every mutation is staged by default so a human can review it before it becomes a live skill. The agent sees a pending response; you approve or reject via the SkillManager API or the skill_manage tool.

When to use propose=False


How It Works

The skill management system provides six mutation actions, three approval actions, and four lifecycle actions:
ActionPurposeStaged by default?
createCreate new skills✅ Yes
editReplace skill content✅ Yes
patchTargeted find/replace✅ Yes
deleteArchive by default (recoverable)✅ Yes
write_fileAdd skill resources✅ Yes
remove_fileDelete skill files✅ Yes
archiveMove skill to archive store✅ Yes
restoreReturn archived skill to active✅ Yes
list_archivedList archived skills— (read-only)
rollbackUndo last edit/patch✅ Yes
pendingList staged mutations— (read-only)
approveApply a staged mutation— (approval action)
rejectDiscard a staged mutation— (approval action)

Configuration

Constructor & environment

OptionTypeDefaultDescription
SkillManager(write_approval=...)bool | NoneNone (env-resolved)Per-instance override for staging policy
SKILL_WRITE_APPROVAL env varstr1 (true)Set to 0/false/off/no/disabled to disable staging by default
SKILL_MAX_PENDING env varint100Maximum pending proposals before new ones are rejected
propose (per call)bool | NoneNone (inherits)True → stage; False → write directly; None → use manager default
Precedence: explicit propose= arg > SkillManager(write_approval=...) > SKILL_WRITE_APPROVAL env var > default True
import os
os.environ["SKILL_WRITE_APPROVAL"] = "0"   # disable staging globally

from praisonaiagents import SkillManager

# Per-instance override
mgr = SkillManager(write_approval=True)     # always stage, regardless of env

# Per-call override
mgr.create_skill("my-skill", "# content", propose=False)  # write directly this once

Skill management actions

ActionRequired ArgsOptional ArgsWhat it does
createname, contentcategory, proposeCreate a new skill (staged by default)
editname, contentproposeReplace an existing skill’s SKILL.md body
patchname, old_string, new_stringfile_path, replace_all, proposeFuzzy find-and-replace within a skill file
deletenamehard, proposeArchives by default; pass hard=True for permanent removal
write_filename, file_path, file_contentproposeAdd/overwrite a file inside the skill
remove_filename, file_pathproposeDelete a file from within the skill
archivenameproposeMove the skill to the recoverable archive store
restorenameskill_dir, proposeBring an archived skill back into active use
list_archivedList names of archived skills
rollbacknameproposeUndo the most recent edit/patch (single-step)
pendingname (filter)Returns list of staged mutations
approvename (id or skill name)Apply the staged mutation
rejectname (id or skill name)Discard the staged mutation

Python API Reference

Response shapes

Staged (default) — propose=True:
{
    "success": True,
    "status": "pending",
    "id": "skl-a1b2c3d4",   # short id for approve/reject
    "action": "create",
    "skill": "weekly-summary",
}
Direct write — propose=False:
{"success": True, "skill": "weekly-summary", "path": "/path/to/skill"}
Failure (either mode):
{"success": False, "error": "Error message"}

Mutation methods

from praisonaiagents import SkillManager

mgr = SkillManager()
mgr.discover()

# --- CREATE ---
# Default (staged):
mgr.create_skill("weekly-summary", "# Weekly Summary\nSteps...", category="reporting")
# → {"success": True, "status": "pending", "id": "skl-a1b2c3d4", "action": "create", "skill": "weekly-summary"}

# Direct write (trusted contexts only):
mgr.create_skill("weekly-summary", "# Weekly Summary\nSteps...", category="reporting", propose=False)
# → {"success": True, "skill": "weekly-summary", "path": "/path/to/skill"}

# --- EDIT ---
mgr.edit_skill("weekly-summary", "# Weekly Summary v2\n...")
# → {"success": True, "status": "pending", "id": "skl-b2c3d4e5", "action": "edit", "skill": "weekly-summary"}

mgr.edit_skill("weekly-summary", "# Weekly Summary v2\n...", propose=False)
# → {"success": True, "skill": "weekly-summary"}

# --- PATCH ---
mgr.patch_skill("weekly-summary", old_string="v2", new_string="v3")
# → {"success": True, "status": "pending", "id": "skl-c3d4e5f6", "action": "patch", "skill": "weekly-summary"}

mgr.patch_skill("weekly-summary", old_string="v2", new_string="v3", propose=False)
# → {"success": True, "skill": "weekly-summary", "replacements": 1}

# --- DELETE ---
mgr.delete_skill("weekly-summary")
# → {"success": True, "status": "pending", "id": "skl-d4e5f6g7", "action": "delete", "skill": "weekly-summary"}

mgr.delete_skill("weekly-summary", propose=False)
# → {"success": True, "skill": "weekly-summary", "path": "/path/to/skill"}

# --- WRITE_FILE ---
mgr.write_skill_file("weekly-summary", "scripts/report.py", "print('Weekly report')")
# → {"success": True, "status": "pending", "id": "skl-e5f6g7h8", "action": "write_file", "skill": "weekly-summary"}

mgr.write_skill_file("weekly-summary", "scripts/report.py", "print('Weekly report')", propose=False)
# → {"success": True, "skill": "weekly-summary", "file": "scripts/report.py"}

# --- REMOVE_FILE ---
mgr.remove_skill_file("weekly-summary", "scripts/report.py")
# → {"success": True, "status": "pending", "id": "skl-f6g7h8i9", "action": "remove_file", "skill": "weekly-summary"}

mgr.remove_skill_file("weekly-summary", "scripts/report.py", propose=False)
# → {"success": True, "skill": "weekly-summary", "file": "scripts/report.py"}

# --- ARCHIVE / RESTORE / ROLLBACK ---
mgr.archive_skill("weekly-summary", propose=False)
mgr.list_archived_skills()  # ["weekly-summary"]
mgr.restore_skill("weekly-summary", propose=False)
mgr.rollback_skill("weekly-summary", propose=False)  # undo last edit/patch via .skill.bak

Approval state machine

from praisonaiagents import SkillManager

mgr = SkillManager()

# List pending proposals
pending = mgr.list_pending()
# [{'id': 'skl-a1b2c3d4', 'action': 'create', 'name': 'weekly-summary', 'status': 'pending', 'created_at': '2026-06-24T11:00:00Z'}]

# Approve by ID (preferred) or skill name (most-recent-wins fallback)
mgr.approve("skl-a1b2c3d4")   # by ID
mgr.approve("weekly-summary")  # by skill name

# Reject by ID or skill name
mgr.reject("skl-a1b2c3d4")
mgr.reject("weekly-summary")
Approval requires a valid SKILL.md (since PraisonAI PR #2467). approve() checks that the pending directory contains SKILL.md before moving it into the active skills directory. A pending entry without SKILL.md returns:
❌ Pending '{name}' has no SKILL.md and cannot be approved.
This prevents a stray or partially-written pending directory from clobbering an existing active skill. Malformed .proposal.json metadata no longer blocks approval — it is treated as a legacy create record as long as SKILL.md is present.

SkillMutatorProtocol aliases

SkillManager conforms to SkillMutatorProtocol. Short-form aliases are available alongside the _skill methods:
AliasLong form
mgr.create(name, content, category=None, propose=None)mgr.create_skill(...)
mgr.edit(name, content, propose=None)mgr.edit_skill(...)
mgr.patch(name, old, new, file_path=None, replace_all=False, propose=None)mgr.patch_skill(...)
mgr.delete(name, propose=None)mgr.delete_skill(...)
mgr.write_file(name, file_path, file_content, propose=None)mgr.write_skill_file(...)
mgr.remove_file(name, file_path, propose=None)mgr.remove_skill_file(...)
from praisonaiagents import SkillManager

mgr = SkillManager()

# Short-form aliases — same methods as create_skill, edit_skill, etc.
mgr.create("weekly-summary", "# Weekly Summary\nSteps...")
mgr.patch("weekly-summary", old_string="v1", new_string="v2", propose=False)

Pending Store & Audit Log

Both artefacts live under the user-owned skills base directory (honouring PRAISONAI_HOME):
PathFormatPurpose
<skills_dir>/.pending_skills.jsonJSON map {id: record}Staged mutations awaiting approval
<skills_dir>/.skill_audit.logJSON-linesAppend-only audit: proposed / approved / approval_failed / rejected events
Pending record shape:
{
  "id": "skl-a1b2c3d4",
  "action": "create",
  "name": "weekly-summary",
  "status": "pending",
  "created_at": "2026-06-24T11:00:00Z",
  "payload": { }
}
Audit log line:
{"event": "approved", "timestamp": "2026-06-24T11:05:00Z", "id": "skl-a1b2c3d4", "action": "create", "name": "weekly-summary"}
approve only consumes the pending record and audits “approved” if the apply succeeds. On failure, the record is kept and the audit logs “approval_failed” so you can retry.

Visibility rules

list_pending() only surfaces a pending directory when it carries a real SKILL.md. Stray temp directories or directories that contain nothing but a malformed .proposal.json are silently excluded so they cannot be advertised as approvable.
Pending dir contentsSurfaced by list_pending()?
SKILL.md + valid .proposal.json✅ Yes (action read from proposal)
SKILL.md, no proposal✅ Yes (treated as create)
SKILL.md + malformed .proposal.json✅ Yes (fallback to create)
Malformed .proposal.json, no SKILL.md❌ No
Empty / stray directory❌ No

Storage Location

Base-dir change (PR #2236): create_skill now always writes to the user-owned get_skills_dir() (which honours PRAISONAI_HOME). Previously it could fall back to an admin-managed location like /etc/praison/skills. Set PRAISONAI_HOME to control where skills land.
Skills are stored in directories according to this precedence order (highest to lowest):
  1. Project: ./.praisonai/skills/ — and ./.claude/skills/ for compatibility
  2. Ancestor walk: any .praisonai/skills or .claude/skills in parent directories
  3. User: ~/.praisonai/skills/ (or $PRAISONAI_HOME/skills/)
  4. System (Unix): /etc/praison/skills/
Archive store: Archived skills are moved to <first_skill_dir>/../skills_archive/. If a name already exists in the archive, a UTC timestamp suffix is appended (e.g. weekly-summary.20240624153012).

Security Guards

GuardPurposeImplementation
Size LimitsPrevent resource exhaustion100KB SKILL.md, 1MB files
Name ValidationSecure identifiers[a-z0-9][a-z0-9._-]* pattern, 64 char limit
Path ValidationPrevent traversalBlock .., absolute paths, encoded attacks
Atomic WritesPrevent corruptionTemp file + rename operations
Allowed SubdirsRestrict file placementreferences/, templates/, scripts/, assets/ only
Max PendingPrevent accumulationSKILL_MAX_PENDING=100 (101st returns error)

Common Patterns

Complete approval flow

from praisonaiagents import Agent, SkillManager

# Agent proposes
agent = Agent(
    name="Learning Assistant",
    instructions="When users teach you something, save it as a skill.",
    tools=["skill_manage", "skills_list", "skill_view"]
)

agent.start("Here's how to analyze CSV files: load with pandas, check for nulls, then create summary stats")
# Agent calls: skill_manage(action="create", name="csv-analysis", content="...")
# Returns: {"status": "pending", "id": "skl-a1b2c3d4"}

# Human reviews and approves
mgr = SkillManager()
print(mgr.list_pending())
mgr.approve("skl-a1b2c3d4")
print("Skill is now live!")

Local dev — disable staging

import os
os.environ["SKILL_WRITE_APPROVAL"] = "0"

from praisonaiagents import Agent

agent = Agent(
    name="Dev Agent",
    instructions="Save skills immediately.",
    tools=["skill_manage", "skills_list", "skill_view"]
)

agent.start("Create a skill called 'csv-analysis' …")
# Writes to disk directly — no pending step

Skill evolution with approval

from praisonaiagents import SkillManager

mgr = SkillManager()
mgr.discover()

# Propose a patch — staged
result = mgr.patch_skill("csv-analysis", 
    old_string="2. Check for nulls",
    new_string="2. Handle missing headers\n3. Check for nulls")
print(result["id"])  # skl-…

# Apply it
mgr.approve(result["id"])

Safe edit with rollback

from praisonaiagents import SkillManager

mgr = SkillManager()
mgr.discover()

result = mgr.edit_skill("csv-analysis", "# CSV Analysis v2\nUpdated steps...", propose=False)
if result["success"]:
    rollback = mgr.rollback_skill("csv-analysis", propose=False)
    print("Reverted to previous version:", rollback)

Using the skill_manage tool actions directly

from praisonaiagents.tools.skill_tools import skill_manage
import json

# Propose creation
result = json.loads(skill_manage(action="create", name="my-skill", content="# My Skill"))
print(result["id"])  # skl-…

# List pending
pending = json.loads(skill_manage(action="pending", name=None))
print(pending["pending"])

# Approve
json.loads(skill_manage(action="approve", name="skl-a1b2c3d4"))

# Reject
json.loads(skill_manage(action="reject", name="skl-a1b2c3d4"))

Skill Bundles

Group related skills into named, reusable sets with praisonai skills bundle list and praisonai skills bundle show <name>. Select a whole bundle from an agent with skills=["@backend-dev"]. See Skill Bundles for the full guide.

Troubleshooting

ImportError Fix: If you see ImportError: cannot import name 'get_default_skill_directories', upgrade praisonaiagents — this was fixed in MervinPraison/PraisonAI#1687. The function was renamed from get_default_skill_directories to get_default_skill_dirs.
“max pending reached” error: You have 100 or more staged mutations. Run mgr.list_pending() and approve or reject existing proposals before creating new ones. Raise the limit with SKILL_MAX_PENDING=200 if needed.
“Pending ‘{name}’ has no SKILL.md and cannot be approved”: The pending directory exists but is missing SKILL.md. Either delete the directory under <skills_dir>/pending/<name>/ or restore a SKILL.md before retrying approve(). Introduced in PraisonAI PR #2467 to stop empty/stray pending dirs from clobbering live skills.

Best Practices

In any unattended context — Slack bots, webhooks, scheduled agents — leave propose=True (the default). This ensures no skill mutation reaches disk without a human seeing it first. Set SKILL_WRITE_APPROVAL=0 only on machines you control and trust.
Direct writes are convenient for rapid iteration in local development or a controlled CI pipeline where you own the environment. Never disable staging for agents exposed to external users or production traffic.
approve("skl-a1b2c3d4") is exact — it targets one specific proposal. approve("weekly-summary") uses a most-recent-wins fallback and can silently target the wrong proposal if multiple are pending for the same skill.
All skill operations are workspace-contained and use atomic writes via temp files. Never bypass name validation or path checks. Skills inherit workspace security automatically.
All skill operations return detailed JSON results with success flags and error messages. Always check result["success"] before proceeding. On approval failure, the pending record is kept — check mgr.list_pending() and retry.
A pending directory is only approvable when it contains SKILL.md. If you stage skills outside the skill_manage tool (e.g. via a script that writes to <skills_dir>/pending/<name>/), write SKILL.md first — approval will fail until it is present, and list_pending() will not show the entry.

Skill Lifecycle

Provenance, telemetry, archive/restore, and rollback for agent-created skills

Agent Skills

Load and configure SKILL.md skills on agents

Workspace

How workspace containment secures skill operations

Skill Capability Gates

Capability requirements and enforcement for skills

Self-Improving Agents

Auto-trigger skill_manage after each task with self_improve=True