OpenFn / OpenFn/apollo

Add skills as slash commands

Open
#614 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Jupyter Notebook
Stars
5
Forks
10
Avg merge
2d 20h
Merged PRs (30d)
17

Description

Summary

Add user-invocable "skills" to the AI assistant, starting with two standard slash commands: /diagnose (debug a failed run) and /qa (quality-assurance review of a workflow). A skill is a reusable instruction set (a markdown file, following Anthropic's Agent Skills format) that augments the model's context for a turn when the user explicitly invokes it.

v0 is almost entirely Apollo work and ships two immutable standard skills with near-zero frontend. The design is deliberately shaped so that v1 (user-defined, project-scoped skills stored in Lightning) and the longer-term version (multi-file skills with assets, fetched via an MCP server) are extensions, not rewrites.

Design principles (why v0 is built this way)

  1. Skills are user content, so they live in Lightning. Like workflows, custom skills need project scoping, an editing UI, versioning, audit and backup. Lightning already owns the tenant boundary; Apollo authenticates instances, not projects, and must not become a second place where project-level isolation is enforced. Apollo stays stateless and stores no tenant data; it receives skills per request (inline at first, by reference later via MCP).

  2. Standard skills are immutable and versioned in Apollo. They are part of the agent's behaviour, so they live in the Apollo repo next to the prompts, evolve with the agent code, and upgrade for everyone on deploy. Users never edit them in place. This mirrors how mainstream agent tooling treats the skills it ships with: built-ins are read-only, and users extend by adding their own.

  3. Invocation and definition are separate concepts.

    • Invocation: "use skill X this turn" (v0 needs only this).
    • Definition: the skill's content (v1, when Lightning stores custom skills).
  4. Follow the standard Agent Skills format from day one. A skill is a folder whose entry point is SKILL.md (YAML frontmatter with name and description, then the instructions), optionally with references/ and assets/ later. In v0 every skill is just the one SKILL.md, but storing built-ins in the real format means a v0 built-in, a v1 custom skill, and a future MCP-fetched skill are the same artifact.

  5. Follow standard slash-command semantics as established by mainstream agent tooling: command recognized only at the start of the message, detected by the frontend against a known list (the backend never parses commands out of free text), the rest of the message is the arguments, the raw message stays in the visible transcript, one skill per message, and the skill's instructions persist naturally via conversation history (no session state).

v0: two standard skills, Apollo-side

Scope
  • Two immutable standard skills: /diagnose and /qa.
    • /diagnose: diagnose a failed workflow run from logs, dataclips and workflow YAML. ("Diagnose" rather than "debug" because the input is a failed run, not just code.)
    • /qa: run a QA review of the current workflow (prompts adapted from the existing internal QA Claude project).
  • Frontend: detect the slash command, send one new payload field, and show a small "did you know" hint for the two commands. No skill management UI.
Skill content
  • /diagnose: to be written in collaboration with someone who knows the global agent's architecture (e.g. how to call subagents) and someone familiar with debugging workflows in practice.
  • /qa: based on Hunter's QA agent project.
Payload change (global_chat)

One new optional field:

"skill": { "name": "diagnose" }
  • Lightning detects /diagnose ... or /qa ... at the start of the message (hardcoded list in v0), sends content as typed plus the skill field.
  • No skill content is sent in v0: standard skills are already on Apollo.
  • Unknown skill name returns a structured ApolloError.
Behaviour inside global_chat
payload.skill absent → unchanged: RouterAgent → subagent / PlannerAgent
payload.skill present → skip the router, load skills/<name>/SKILL.md,
                        strip the command token from the user message,
                        inject the skill body as a preamble to the user turn,
                        hand the turn to the PlannerAgent

Design points:

  • Skill invocation bypasses the router. The router guesses intent; a slash command states it. Explicit invocation should be deterministic and testable. Both v0 skills are multi-step tasks, which is what the planner is for. If a future skill needs a different target, that becomes a route: key in the SKILL.md frontmatter (a one-line extension).
  • Skill content is injected into the turn, not the permanent system prompt. It becomes part of that user turn and therefore of the returned history, so follow-up turns keep working without re-invoking.
  • One skill per message. The standard convention; keeps the mental model and the implementation simple.
Repo layout (Apollo)
services/global_chat/
  skills/
    diagnose/
      SKILL.md
    qa/
      SKILL.md
  skill_registry.py      # loads skills/ at startup; v1: overlays payload customs
  global_chat.py         # if payload.skill: registry lookup → inject → planner
  router.py              # untouched
  planner.py             # untouched
  prompts.yaml           # untouched; skills are content, prompts.yaml is agent plumbing

Storing built-ins as real SKILL.md folders (rather than entries in prompts.yaml) is slightly unusual for this repo but deliberate: it keeps built-ins byte-for-byte identical to what users will store in v1.

Limits

HTTP is not the constraint in v0 (Bun's default body limit is 128MB and the bridge already handles large payloads via temp files); the model context window is. Guideline limits: 64KB per skill file, ~10 skills per project, in line with Anthropic's guidance to keep a SKILL.md body under roughly 5k words.

Tests
  • test_pass_fail.py: unknown skill rejected; router bypassed on invocation; command token stripped; skill text present in the constructed turn; normal path unaffected when skill is absent.
  • test_qualitative.py: real /diagnose and /qa runs against sample failed runs / workflows for human review.
v0 task list
  • Apollo: skill_registry.py loader + skills/ folder with the two SKILL.md files
  • Apollo: skill payload field, validation, router bypass, turn injection
  • Apollo: update PAYLOAD_SPEC.md
  • Apollo: pass/fail + qualitative tests
  • Prompts: adapt the QA project prompts into qa/SKILL.md; write diagnose/SKILL.md
  • Lightning: slash-command detection at start of message (hardcoded list) + skill field in the request
  • Lightning: small UI hint advertising the two commands
Estimate

The Apollo-side mechanism (registry, payload field, router bypass, injection, spec, structural tests) is roughly one focused day for someone familiar with global_chat, assuming skill content exists. Prompt quality for /diagnose and /qa (adapting the QA prompts, iterating against real failed runs, qualitative review) is a separate, explicitly time-boxed pass on top; after day one, skills can be improved by editing markdown without touching code.

v1: custom project-scoped skills (summary)

  • Lightning gets a skills table (project_id, name, description, body) and a minimal CRUD UI. Custom skills are project-scoped with the same isolation guarantees as workflows.

  • The payload gains a second, separate field carrying definitions for custom skills:

    "skills": [
      {
        "name": "our-team-review",
        "description": "Review a workflow against our team's conventions",
        "files": [ { "path": "SKILL.md", "content": "..." } ]
      }
    ]
    

    The files array is a one-element list in v1 (content is just a string in a field), but the shape is the one that later carries multiple files and asset references without a breaking change.

  • Apollo overlays payload skills onto its built-in registry per request. Name collisions with standard skills are rejected, keeping a clean separation between shipped and user content.

  • "Customize a standard skill" = copy-on-write: the UI copies the standard skill's markdown into the project's skills table under a new name, and the user edits the copy. Open question to resolve in v1 planning: how Lightning obtains the current standard skill content to copy (e.g. an Apollo endpoint listing standard skills). Noted, not blocking.

  • Slash-command detection generalizes from the hardcoded list to standard + the project's custom skill names.

Future: multi-file skills and MCP (summary)

  • Skills become full folders per the Agent Skills format: SKILL.md as entry point plus references/ and assets/. Text files ride in the same files array; binary assets become references (URL + hash) rather than inline content.
  • Storage-wise a "folder" is not a filesystem: Lightning's skills table gains a skill_files table (skill_id, path, content), with the hierarchy living in the path strings. Text stays in Postgres; binary assets go to object storage with a pointer row (path, storage key, hash). Standard split, no migration of v1 data.
  • An MCP server between Lightning and Apollo replaces push-inline with fetch-by-reference: the request carries only skill manifests (name + description) and the agent loads a skill's files on demand (progressive disclosure). Storage stays in Lightning throughout; only the transport changes, which is why the v0/v1 payload shape needs no overhaul.
  • Frontend grows richer skill editing, including uploading documents as skill assets.

Explicitly out of scope for v0

  • Any Lightning database changes or skill CRUD UI
  • Custom or editable skills
  • MCP transport

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with services/global_chat/global_chat.py and the proposed skill_registry.py, then read test_pass_fail.py and PAYLOAD_SPEC.md. Trace the existing global_chat payload through router.py and planner.py before implementing the v0 path. Done means both standard skills load, invocation bypasses the router and injects the skill text, unknown skills fail, and the normal path remains unchanged.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
ai, api, backend
Issue type
Feature
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.