
Felo Slides
- 896 installs
- 241 repo stars
- Updated July 22, 2026
- felo-inc/felo-skills
felo-slides is a Claude Code agent skill that turns topics, outlines, or notes into shareable PowerPoint decks using the Felo PPT Task API for developers who need automated presentation files.
About
felo-slides is a felo-inc/felo-skills integration that calls the Felo PPT Task API from Claude Code to create presentation slide decks. The workflow checks API keys, creates a generation task, polls until completion, and returns a final ppt_url link developers can share. Use felo-slides when prompts ask to create, make, generate, or export slides or PPT files from a topic, outline, notes, or explicit /felo-slides command, including Chinese presentation requests. Page-count requirements and bilingual trigger keywords are documented so agents handle end-to-end deck creation without manual PowerPoint editing.
- Generates full PPT decks from a topic, outline or notes
- Supports explicit page count and formatting requirements
- Handles API key validation, task creation, polling and returns final ppt_url
- Activated by natural language requests or the /felo-slides command
- Optimized for both English and Chinese slide-generation prompts
Felo Slides by the numbers
- 896 all-time installs (skills.sh)
- +27 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #302 of 1,335 Generative Media skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/felo-inc/felo-skills --skill felo-slidesAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 896 |
|---|---|
| repo stars | ★ 241 |
| Security audit | 3 / 3 scanners passed |
| Last updated | July 22, 2026 |
| Repository | felo-inc/felo-skills ↗ |
How do you generate PowerPoint slides from an outline?
Instantly turn topics, outlines or notes into complete, shareable PowerPoint slide decks using the Felo PPT Task API inside Claude Code.
Who is it for?
Developers in Claude Code who need fast, API-driven PowerPoint decks from notes without manual slide layout work.
Skip if: Teams requiring on-premise, code-only Reveal.js decks with zero external presentation API dependencies.
When should I use this skill?
A user asks to create slides, generate a PPT, export a presentation, or invokes /felo-slides with a topic or outline.
What you get
Completed Felo PPT task, shareable ppt_url link, and generated slide deck ready for download or presentation.
- Shareable ppt_url
- Generated slide deck
Files
Felo Slides Skill
When to Use
Trigger this skill for requests about creating presentation files:
- Create/generate slides from a topic or outline
- Turn notes into a PPT deck
- Build a presentation with a page count requirement
- Export presentation content into a shareable slide link
Trigger keywords:
- Chinese prompts about making slides or presentations
- English: slides, PPT, presentation deck, generate presentation
- Explicit commands:
/felo-slides, "use felo slides"
Do NOT use this skill for:
- Real-time information lookup (use
felo-search) - Questions about local codebase files
- Pure text tasks that do not require slide generation
Setup
1. Get API key
1. Visit felo.ai 2. Open Settings -> API Keys 3. Create and copy your API key
2. Configure environment variable
Linux/macOS:
export FELO_API_KEY="your-api-key-here"Windows PowerShell:
$env:FELO_API_KEY="your-api-key-here"How to Execute
Use Bash tool commands and follow this workflow exactly.
Step 1: Precheck API key
if [ -z "$FELO_API_KEY" ]; then
echo "ERROR: FELO_API_KEY not set"
exit 1
fiIf key is missing, stop and return setup instructions.
Step 2: Run Node Script (create + poll)
Use the bundled script (no jq dependency):
node felo-slides/scripts/run_ppt_task.mjs \
--query "USER_PROMPT_HERE" \
--interval 10 \
--max-wait 1800 \
--timeout 60To apply a specific theme, first list available themes with felo ppt-themes, then pass the theme ID:
node felo-slides/scripts/run_ppt_task.mjs \
--query "USER_PROMPT_HERE" \
--theme "THEME_ID_HERE" \
--interval 10 \
--max-wait 1800 \
--timeout 60Script behavior:
- Creates task via
POST https://openapi.felo.ai/v2/ppts - Supports optional
--theme <id>to apply a PPT theme (sendsppt_config.ai_theme_id) - Supports optional
--livedoc-id <id>to reuse an existing LiveDoc instead of auto-creating a new one - Supports optional
--task-id <id>to resume polling an existing task (skips creation) - Polls via
GET https://openapi.felo.ai/v2/tasks/{task_id}/historical - Treats
COMPLETED/SUCCESSas success terminal (case-insensitive) - Treats
FAILED/ERRORas failure terminal - Stops polling immediately on terminal status
- Prints
ppt_urlon success (fallback:live_doc_url)
Optional debug output:
node felo-slides/scripts/run_ppt_task.mjs \
--query "USER_PROMPT_HERE" \
--interval 10 \
--max-wait 1800 \
--json \
--verboseThis outputs structured JSON including:
task_idtask_statusppt_urllive_doc_urllivedoc_short_idppt_business_iderror_message
Step 3: Confirm terminal status
The script polls the historical endpoint automatically. Wait for it to exit before responding:
- Success: terminal status is
COMPLETEDorSUCCESS, and the script prints
ppt_url (fallback: live_doc_url)
- Failure: terminal status is
FAILEDorERROR; return the error format below - Timeout: include the
task_idand tell the user to resume with--task-id
instead of creating a duplicate PPT task
Step 4: Return structured result
On success, return:
ppt_urlimmediately (script default output, fallbacklive_doc_url)- if
--jsonis used, also includetask_id, terminal status, and optional metadata
Output Format
Use this response structure:
## PPT Generation Result
- Task ID: <task_id>
- Status: <status>
- PPT URL: <ppt_url>
- Live Doc URL: <live_doc_url or N/A>
## Notes
- livedoc_short_id: <value or N/A>
- ppt_business_id: <value or N/A>Error format:
## PPT Generation Failed
- Error Type: <error code or category>
- Message: <readable message>
- Suggested Action: <next step>Error Handling
Known API error codes:
INVALID_API_KEY(401): key invalid or revokedPPT_TASK_CREATE_FAILED(502): create task downstream failedPPT_TASK_QUERY_FAILED(502): query task downstream failed
Timeout handling:
- If timeout reached, return last known status and instruct user to retry later
- Include
task_idso user can query again - IMPORTANT: To resume a timed-out task, use
--task-idinstead of--queryto avoid creating a duplicate PPT:
node felo-slides/scripts/run_ppt_task.mjs \
--task-id "TASK_ID_HERE" \
--interval 10 \
--max-wait 1800Important Notes
- Always execute this skill when user intent is slide generation.
- Always return
task_idso follow-up queries can continue from the same task. - Do not claim completion without a terminal status.
- Keep API calls minimal: create once, then poll.
References
{
"name": "Felo Slides",
"tagline": "Generate PPT/slides with Felo PPT Task API in Claude Code",
"description": "Felo Slides creates presentation decks from a prompt using the Felo PPT Task API: API key check, task creation, polling, theme support, task resume, and final ppt_url output. Use from Claude Code when users ask to create or export slides/PPT.",
"category": "productivity",
"tags": ["felo", "slides", "ppt", "presentation", "api", "claude-code"],
"version": "1.0.1",
"license": "MIT",
"pricing": "free",
"support_url": "https://github.com/Felo-Inc/felo-skills/issues",
"homepage": "https://github.com/Felo-Inc/felo-skills"
}
MIT License
Copyright (c) 2026 Felo Slides Skill Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Felo Slides Skill for Claude Code
Generate presentation slides with the Felo PPT Task API (asynchronous workflow).
Features
- Generate a PPT deck from a single prompt
- Poll task status automatically until completion/failure/timeout
- Return
ppt_urlimmediately when the task is completed (fallback tolive_doc_url) - Return
task_idfor follow-up tracking
Quick Start
1) Install the skill
一键安装(推荐):
npx skills add Felo-Inc/felo-skills --skill felo-slides手动安装: 若上述命令不可用,从本仓库复制到 Claude Code 的 skills 目录:
# Linux/macOS
cp -r felo-slides ~/.claude/skills/
# Windows (PowerShell)
Copy-Item -Recurse felo-slides "$env:USERPROFILE\.claude\skills\"(Clone the repo first if needed: git clone https://github.com/Felo-Inc/felo-skills.git.)
2) Configure API key
Create an API key at felo.ai -> Settings -> API Keys, then set:
# Linux/macOS
export FELO_API_KEY="your-api-key-here"# Windows PowerShell
$env:FELO_API_KEY="your-api-key-here"3) Trigger the skill
- Intent trigger: "Create a 10-slide product pitch deck"
- Explicit trigger:
/felo-slides your topic
API Workflow
Based on Felo v2 PPT Task API:
1. Create task: POST /v2/ppts 2. Query status (optional): GET /v2/tasks/{task_id}/status 3. Query historical/result: GET /v2/tasks/{task_id}/historical
The skill polls every 10 seconds (max wait 1800 seconds). It stops immediately on COMPLETED/SUCCESS and returns ppt_url (fallback live_doc_url).
Internal script example:
node felo-slides/scripts/run_ppt_task.mjs --query "Felo product intro, 3 slides" --interval 10 --max-wait 1800Troubleshooting
FELO_API_KEY is missing
Set the environment variable and restart the Claude Code session.
INVALID_API_KEY
The key is invalid or revoked. Generate a new key from felo.ai.
Task keeps running for too long
The task may still be processing. Retry later with the same context, or run the script with --verbose.
Task completed but no ppt_url / live_doc_url
Use the returned task_id to query historical endpoint again.
Links
#!/usr/bin/env node
const DEFAULT_API_BASE = 'https://openapi.felo.ai';
const DEFAULT_INTERVAL_SEC = 10;
const DEFAULT_MAX_WAIT_SEC = 1800;
const DEFAULT_TIMEOUT_SEC = 60;
function usage() {
console.error(
[
'Usage:',
' node felo-slides/scripts/run_ppt_task.mjs --query "your prompt" [options]',
'',
'Options:',
' --query <text> PPT prompt (required unless --task-id is given)',
' --task-id <id> Resume polling an existing task (skip creation)',
' --theme <id> PPT theme ID (from ppt-themes)',
' --livedoc-id <id> Reuse an existing LiveDoc instead of auto-creating one',
' --interval <seconds> Poll interval, default 10',
' --max-wait <seconds> Max wait time, default 1800',
' --timeout <seconds> Request timeout, default 60',
' --json Print JSON output',
' --verbose Print polling status to stderr',
' --help Show this help',
].join('\n')
);
}
function parseArgs(argv) {
const out = {
query: '',
taskId: '',
theme: '',
livedocId: '',
intervalSec: DEFAULT_INTERVAL_SEC,
maxWaitSec: DEFAULT_MAX_WAIT_SEC,
timeoutSec: DEFAULT_TIMEOUT_SEC,
json: false,
verbose: false,
};
for (let i = 0; i < argv.length; i += 1) {
const a = argv[i];
if (a === '--help' || a === '-h') {
out.help = true;
} else if (a === '--json') {
out.json = true;
} else if (a === '--verbose' || a === '-v') {
out.verbose = true;
} else if (a === '--query') {
out.query = argv[i + 1] ?? '';
i += 1;
} else if (a === '--task-id') {
out.taskId = argv[i + 1] ?? '';
i += 1;
} else if (a === '--theme') {
out.theme = argv[i + 1] ?? '';
i += 1;
} else if (a === '--livedoc-id') {
out.livedocId = argv[i + 1] ?? '';
i += 1;
} else if (a === '--interval') {
out.intervalSec = Number.parseInt(argv[i + 1] ?? '', 10);
i += 1;
} else if (a === '--max-wait') {
out.maxWaitSec = Number.parseInt(argv[i + 1] ?? '', 10);
i += 1;
} else if (a === '--timeout') {
out.timeoutSec = Number.parseInt(argv[i + 1] ?? '', 10);
i += 1;
} else if (!a.startsWith('-') && !out.query) {
out.query = a;
}
}
if (!Number.isFinite(out.intervalSec) || out.intervalSec <= 0) out.intervalSec = DEFAULT_INTERVAL_SEC;
if (!Number.isFinite(out.maxWaitSec) || out.maxWaitSec <= 0) out.maxWaitSec = DEFAULT_MAX_WAIT_SEC;
if (!Number.isFinite(out.timeoutSec) || out.timeoutSec <= 0) out.timeoutSec = DEFAULT_TIMEOUT_SEC;
return out;
}
function normalizeStatus(v) {
return String(v ?? '').trim().toUpperCase();
}
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function getMessage(payload) {
return (
payload?.message ||
payload?.error ||
payload?.msg ||
payload?.code ||
'Unknown error'
);
}
function isApiError(payload) {
const status = payload?.status;
const code = payload?.code;
if (typeof status === 'string' && status.toLowerCase() === 'error') return true;
if (typeof code === 'string' && code && code.toUpperCase() !== 'OK') return true;
return false;
}
async function fetchJson(url, init, timeoutMs) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
const res = await fetch(url, { ...init, signal: controller.signal });
let body = {};
try {
body = await res.json();
} catch {
body = {};
}
if (!res.ok) {
throw new Error(`HTTP ${res.status}: ${getMessage(body)}`);
}
if (isApiError(body)) {
throw new Error(getMessage(body));
}
return body;
} finally {
clearTimeout(timer);
}
}
function extractTaskUrls(historicalData, createData) {
const pptUrl = historicalData?.ppt_url || '';
const liveDocUrl =
historicalData?.live_doc_url ||
(historicalData?.live_doc_short_id || historicalData?.livedoc_short_id || createData?.livedoc_short_id
? `https://felo.ai/livedoc/${historicalData?.live_doc_short_id || historicalData?.livedoc_short_id || createData?.livedoc_short_id}`
: '');
return {
pptUrl,
liveDocUrl,
displayUrl: pptUrl || liveDocUrl,
};
}
async function createTask(apiKey, apiBase, query, timeoutMs, theme, livedocId) {
const reqBody = { query };
if (theme) {
reqBody.ppt_config = { ai_theme_id: theme };
}
if (livedocId) {
reqBody.livedoc_short_id = livedocId;
}
const payload = await fetchJson(
`${apiBase}/v2/ppts`,
{
method: 'POST',
headers: {
Accept: 'application/json',
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(reqBody),
},
timeoutMs
);
const data = payload?.data ?? {};
if (!data.task_id) {
throw new Error('Unexpected response: missing task_id');
}
return data;
}
async function queryHistorical(apiKey, apiBase, taskId, timeoutMs) {
const payload = await fetchJson(
`${apiBase}/v2/tasks/${encodeURIComponent(taskId)}/historical`,
{
method: 'GET',
headers: {
Accept: 'application/json',
Authorization: `Bearer ${apiKey}`,
},
},
timeoutMs
);
return payload?.data ?? {};
}
async function main() {
const args = parseArgs(process.argv.slice(2));
if (args.help) {
usage();
process.exit(0);
}
if (!args.query && !args.taskId) {
usage();
process.exit(1);
}
const apiKey = process.env.FELO_API_KEY?.trim();
if (!apiKey) {
console.error('ERROR: FELO_API_KEY not set');
process.exit(1);
}
const apiBase = (process.env.FELO_API_BASE?.trim() || DEFAULT_API_BASE).replace(/\/$/, '');
const timeoutMs = args.timeoutSec * 1000;
const intervalMs = args.intervalSec * 1000;
const maxWaitMs = args.maxWaitSec * 1000;
let createData = {};
let taskId;
if (args.taskId) {
// Resume polling an existing task
taskId = args.taskId;
if (args.verbose) {
console.error(`Resuming task: ${taskId}`);
}
} else {
// Create a new task
createData = await createTask(apiKey, apiBase, args.query, timeoutMs, args.theme, args.livedocId || undefined);
taskId = createData.task_id;
if (args.verbose) {
console.error(`Task ID: ${taskId}`);
}
}
const startAt = Date.now();
let lastStatus = '';
while (Date.now() - startAt <= maxWaitMs) {
const historicalData = await queryHistorical(apiKey, apiBase, taskId, timeoutMs);
const taskStatus = normalizeStatus(historicalData.task_status || historicalData.status);
const urls = extractTaskUrls(historicalData, createData);
lastStatus = taskStatus || 'UNKNOWN';
if (args.verbose) {
const elapsedSec = Math.floor((Date.now() - startAt) / 1000);
console.error(`[${elapsedSec}s] Status: ${lastStatus}`);
}
if (taskStatus === 'COMPLETED' || taskStatus === 'SUCCESS') {
if (!urls.displayUrl) {
throw new Error('Task completed but no ppt_url/live_doc_url is available');
}
if (args.json) {
console.log(
JSON.stringify(
{
status: 'ok',
data: {
task_id: taskId,
task_status: taskStatus,
ppt_url: urls.pptUrl || null,
live_doc_url: urls.liveDocUrl || null,
livedoc_short_id: createData.livedoc_short_id ?? historicalData.live_doc_short_id ?? historicalData.livedoc_short_id ?? null,
ppt_business_id: createData.ppt_business_id ?? null,
},
},
null,
2
)
);
} else {
console.log(urls.displayUrl);
}
return;
}
if (taskStatus === 'FAILED' || taskStatus === 'ERROR') {
throw new Error(`Task finished with status: ${taskStatus}`);
}
await sleep(intervalMs);
}
throw new Error(`Timed out after ${args.maxWaitSec}s. Last status: ${lastStatus || 'UNKNOWN'}`);
}
main().catch((err) => {
console.error(`ERROR: ${err?.message || err}`);
process.exit(1);
});
Related skills
FAQ
What API does felo-slides use?
felo-slides uses the Felo PPT Task API inside Claude Code. The skill verifies the API key, creates a generation task, polls for completion, and returns the final ppt_url for the finished slide deck.
What triggers felo-slides in Claude Code?
felo-slides triggers on requests to create, make, generate, or export slides or PPT decks, including the /felo-slides command and Chinese presentation prompts. Developers can pass a topic, outline, notes, or required page count.
Is Felo Slides safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.