Skip to content

[POC] feat: implement skill management in tools#6362

Open
prd-hoang-doan wants to merge 1 commit intoFlowiseAI:mainfrom
prd-hoang-doan:feat/skill-tool-poc
Open

[POC] feat: implement skill management in tools#6362
prd-hoang-doan wants to merge 1 commit intoFlowiseAI:mainfrom
prd-hoang-doan:feat/skill-tool-poc

Conversation

@prd-hoang-doan
Copy link
Copy Markdown
Contributor

Proof-of-Concept: Skill management in tools

Ticket:

Flowise Roadmap

Overview:

A Skill is a new authoring primitive in Flowise — a self-contained app composed of markdown prompts, code, data, and binary assets that can be published once and invoked from any Agentflow as one or more LangChain tools. This PR implements the full Skill lifecycle across three packages: server, components, and UI.

Skill-Editor-demo (1)

Database

Column Type Purpose
id uuid (PK) Primary key
workspaceId text
name varchar(255) Display name; unique per workspace
description text (nullable) Optional description of the skill
iconSrc varchar(255) (nullable) UI icon metadata
color varchar(16) (nullable) UI color metadata
fileTree text Entire file/folder tree as a JSON string (SkillFileTree)
contentDigest varchar(64) sha256(canonicalJson({fileTree, sortedNodeDigests})) — invalidates caches on any change
publishedBundleId varchar(64) (nullable) Pointer to the latest published SkillBundle artefact in storage. null until first publish
createdDate datetime Auto-managed creation timestamp
updatedDate datetime Auto-managed update timestamp

Execution Modes

The runtime node collapses into one of two modes depending on environment configuration:

Mode Condition What the LLM sees
Sandbox Shell (bash) E2B_APIKEY is set, SKILL_ALLOW_EXEC ≠ false, and enableBash is on Per-file skill tools enriched with execution recipes + a single bash_ tool that materialises assets under /home/user/skills/ in a sandboxed E2B VM and lets the model run shell commands
Fallback (read-only markdown) E2B not configured, kill-switch flipped, or enableBash=false Per-file skill tools only — compiled markdown is returned verbatim; referenced code/data becomes documentation the model reasons about without execution

Markdown execution flow: Each selected .md file becomes a SkillFileTool. When the LLM calls it, the tool returns the pre-compiled markdown content (with all {{skill.*}} and {{tool.*}} placeholders resolved at publish time) plus an optional tool hint block. Runtime placeholders ({{question}}, {{$vars.*}}) are resolved by Flowise's agent layer after the tool returns.

Bash execution flow: A single SandboxBashTool is registered per skill. On first invocation it lazily boots an E2B VM, materialises all reachable assets (markdown, code, data, binaries) under /home/user/skills/, and executes the command. Results are returned as a JSON envelope { status, stdout, stderr, exitCode, durationMs }. Built-in helper scripts handle document extraction (PDF, DOCX, PPTX, XLSX, HTML, TXT).

File Kind Classification:
Files in the skill tree are classified into four kinds that determine how they are compiled and surfaced at runtime:

Kind Extensions Runtime Behavior
skill .md, .markdown Invocable entry points — placeholders resolved at compile time
code .py, .js, .ts, .sh, .rb, .go, … Passed verbatim to sandbox VM
data .txt, .json, .csv, .yaml, .html, … Text data — readable inside sandbox
binary everything else (.pdf, .png, .xlsx, …) Opaque bytes — surfaced as raw files

How LLM detect the bash command

  • There is compiler to build the bash description which inject the tool hint in the markdown skills
    For example:

flowise:dev: [buildBashToolDescription] Generated bash tool description:
flowise:dev: Run a shell command inside the skill sandbox VM (engine: E2B (Bash session)). Working directory is /home/user; all reachable skill files live under /home/user/skills/ and any artefacts you want to hand back to the user should go into /home/user/output/. Returns a JSON envelope { status, stdout, stderr, exitCode, error?, durationMs, engine }; stdout/stderr are clipped, so pipe large outputs through head/tail or write them to /home/user/output/ and inspect with cat.
flowise:dev:
flowise:dev: Productivity rules — DO NOT default to cat for data files:
flowise:dev: - Always peek first: the per-file commands below are deliberately head/tail probes, not full reads. Run those before anything else.
flowise:dev: - To find specific content, use grep -nE '<pattern>' <path> (or pdfgrep for PDFs, jq for JSON, yq for YAML, xmllint --xpath for XML) — never re-read the whole file.
flowise:dev: - Need the entire file? Confirm size first with wc -c <path> and only then escalate to the explicit cat <path> alternative listed under "Productive commands per family" below.
flowise:dev: - For markdown skill files, you usually already have the content from the per-skill tool response — re-reading them with cat is wasted tokens.
flowise:dev: - Pipe noisy outputs through head -n 200 or write to /home/user/output/ and re-read selectively to stay under the stdout clamp.
flowise:dev:
flowise:dev: Starter commands per file (productive peeks/probes — escalate via "Productive commands per family" below for full reads, search, or query):
flowise:dev: - Execute with Node.js:
flowise:dev: • scoring_algorithm.js → node /home/user/skills/scoring_algorithm.js [args...]
flowise:dev: - Markdown skill files:
flowise:dev: • email-drafter.md → head -n 80 /home/user/skills/email-drafter.md
flowise:dev: • interview-questions.md → head -n 80 /home/user/skills/interview-questions.md
flowise:dev: • resume-screener.md → head -n 80 /home/user/skills/resume-screener.md
flowise:dev: - Plain text data:
flowise:dev: • job-description.txt → head -n 50 /home/user/skills/job-description.txt
flowise:dev:
flowise:dev: Built-in helpers (always available under /home/user/helpers):
flowise:dev: - pdf_extract.py python3 /home/user/helpers/pdf_extract.py # Extract text from PDF (stdlib only — single-page FlateDecode PDFs).
flowise:dev: - docx_extract.py python3 /home/user/helpers/docx_extract.py # Extract paragraph text from a .docx (stdlib only — body paragraphs).
flowise:dev: - xlsx_extract.py python3 /home/user/helpers/xlsx_extract.py # Extract rows from a .xlsx as TSV; multi-sheet workbooks get headers (stdlib only).
flowise:dev: - pptx_extract.py python3 /home/user/helpers/pptx_extract.py # Extract slide text from a .pptx with === Slide N === separators (stdlib only).
flowise:dev: - html_to_text.py python3 /home/user/helpers/html_to_text.py # Strip HTML to plain text; <script>/<style> are dropped, whitespace collapsed (stdlib only).
flowise:dev:
flowise:dev: Productive commands per family (template-only — substitute / / ):
flowise:dev: - Markdown skill files:
flowise:dev: • grep -nE '' — Locate a regex pattern with line numbers; replace <pattern> before issuing.
flowise:dev: • cat — Streams the entire file to stdout — escalate here only after a peek/search proves you need the whole content.
flowise:dev: - Plain text data:
flowise:dev: • grep -nE '' — Locate a regex pattern with line numbers; replace <pattern> before issuing.
flowise:dev: • cat — Streams the entire file to stdout — escalate here only after a peek/search proves you need the whole content.
flowise:dev: • wc -l — Line count — use to size a file before reading it whole.

Testing

storage-skill
  • Storage: local, s3, gcs
  • Database: postgres, mysql, mariadb, sqlite

Demo Recording

- Components: SkillPublishBar, SkillCreateNodeDialog, and reference pickers for files and tools.
- Hooks & Utils: Added useNodeBlobUrl for resource handling, along with tree management, file validation, and extension utilities.
- Feature Integration: Enhanced the Tools view to support full skill lifecycle management.
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a comprehensive "Skills V2" feature, enabling the creation, management, and execution of self-contained skill bundles within an E2B sandbox environment. It includes a full-screen UI editor with file tree and graph visualizations, a server-side compiler for transitive dependency resolution, and enhanced storage providers with raw blob primitives. Feedback focuses on improving code robustness and data integrity, specifically by simplifying boolean logic, ensuring file cleanup in error scenarios, and addressing potential race conditions and state inconsistencies during database and storage operations. Additionally, a suggestion was made to use a more portable UUID generation function in PostgreSQL migrations.

// Capability + manifest. The manifest is only needed for the bash
// path, so building it when the user opted out keeps init() cheap.
// ------------------------------------------------------------------
const bashEnabledByUser = nodeData.inputs?.enableBash === false ? false : true
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The ternary operator can be simplified for better readability, prioritizing code understandability.

const bashEnabledByUser = nodeData.inputs?.enableBash !== false
References
  1. Prioritize code readability and understandability over conciseness.

Comment on lines +278 to +282
try {
fs.unlinkSync(file.path)
} catch {
/* best-effort cleanup */
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The file cleanup logic (fs.unlinkSync) should be placed in a finally block to ensure the temporary file is deleted even if fs.readFileSync fails, preventing file leaks.

try {
    buffer = fs.readFileSync(file.path)
} finally {
    try {
        fs.unlinkSync(file.path)
    } catch {
        /* best-effort cleanup */
    }
}

Comment on lines +37 to +40
const existing = await repo().findOneBy({ workspaceId, name: dto.name.trim() })
if (existing) {
throw new InternalFlowiseError(StatusCodes.CONFLICT, `A skill named "${dto.name}" already exists`)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The check for an existing skill name followed by a save is prone to race conditions. Consider using a database-level unique constraint to ensure data integrity.

Comment on lines +64 to +67
await Promise.all([
writeBuffer(JSON_MIME, buf, `${nodeId}.json`, ...prefix(workspaceId, skillId), 'nodes'),
putNodeMeta(workspaceId, skillId, nodeId, meta)
])
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using Promise.all for writing the payload and metadata can lead to an inconsistent state if one operation fails. Consider sequential execution or a transaction-like approach to ensure both succeed.

Comment on lines +182 to +186
for (const id of toDelete) {
await SkillStorage.deleteNodeAssets(workspaceId, skillId, id)
}

await saveFileTree(workspaceId, skillId, tree)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If saveFileTree fails, the assets are already deleted, leaving the tree in an inconsistent state. Consider wrapping these operations in a transaction or ensuring the tree is updated before deleting assets.

@prd-hoang-doan
Copy link
Copy Markdown
Contributor Author

Hi @HenryHengZJ , @harshit-flowise, @jchui-wd. Could you review the general approach? If you’re happy with this solution, I’m ready to dive back in and handle any further requirements to get this production-ready.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant