Writing a Skill
A skill is a set of automation scripts that drive one app (or a system feature) on a real Android device. When your skill lands in phonebase-skill-hub, any PhoneBase user can pb skills install <your-skill> and run it on their own cloud phone.
This tutorial walks you through writing your first skill in about 10 minutes.
0. Prerequisites
You'll need:
pbCLI installed locally (see Quick Start)- A reachable cloud phone or real Android device
- Node.js 20+
- A GitHub account (to submit the PR)
Fork the repo:
gh repo fork phonebase-cloud/phonebase-skill-hub --clone
cd phonebase-skill-hub1. Directory Layout
Each skill lives under skills/<id>/ and needs at minimum:
skills/
└── your-skill/
├── SKILL.md ← metadata + description (required)
├── scripts/ ← command scripts (required)
│ ├── open.js
│ ├── close.js
│ └── state.js
└── resources/ ← optional assets
└── ic_launcher.webpRules:
<id>is the unique skill identifier, becoming the command prefix (pb your-skill open). Use lowercase, hyphens, no spaces.- Each
.jsfile is a subcommand, filename = command name (open.js→pb your-skill open). - Files starting with
_(e.g._lib.js,_utils.js) are not registered as commands — use them for shared helpers.
2. Write SKILL.md
SKILL.md is a YAML frontmatter + markdown body. The frontmatter is structured metadata (the docs site reads it to generate the listing page and detail page); the body is human-readable context.
Minimum example:
---
name: your-skill
app_name: Your App
description: One-liner describing what this skill does
platform: android
package: com.example.yourapp
---
# Your Skill
Longer description — what it does, dependencies, caveats.Available Fields
| Field | Type | Required | Description |
|---|---|---|---|
name | string | ✅ | Skill name, usually matches the directory name |
description | string | ✅ | One-liner shown on the listing page |
app_name | string | Friendly display name (e.g. Google Play Store). Used for card titles and detail page headings | |
display_name | string | Alias for app_name, higher priority. Only set one | |
version | string | Semver, e.g. 1.0.0 | |
author | string | Author or organization | |
platform | android | ios | web | Target platform. Auto-inferred from package / bundle_id if omitted | |
category | string | Free-form category tag (app-store / social / e-commerce / ...) | |
package | string | Android package name | |
bundle_id | string | iOS bundle id | |
tags | string[] | Arbitrary tags | |
requires | string[] | Dependent skills / capabilities, e.g. ["googleservices"] |
app_name vs display_name
Both do exactly the same thing — provide a friendly display name. The canonical name in this project is app_name; display_name is kept as a compatible alias. If both are set, display_name wins.
Icon
Drop the app icon at resources/ic_launcher.<ext>. The docs site will pick it up automatically:
skills/your-skill/
└── resources/
└── ic_launcher.webp ← webp preferred, png / jpg / svg also supported3. Write Scripts with JSDoc
Every script file is an ES module exporting a default async function. The top of the file should be a standard /** */ JSDoc block describing the command metadata — the docs site parses it to render the command table and argument list.
Tags
| Tag | Example | Purpose |
|---|---|---|
@command <name> | @command search | Command name (defaults to filename) |
@description <text> | @description Search the store | Default-language description |
@description:<lang> <text> | @description:en Search the store | Localized description |
@arg <name>:<type>[!][=<default>] <desc> | @arg keyword:string! Search keyword | Argument declaration |
@arg:<lang> <name> <desc> | @arg:en keyword Search keyword | Localized argument description |
Where <type> is string / int / bool / etc., ! marks required, and =<value> sets a default.
Full Example
/**
* @command search
* @description Search the Play Store via market://search deeplink
* @description:zh 用 market://search deeplink 在 Play Store 里搜索 App
* @arg keyword:string! Search keyword
* @arg:zh keyword 搜索关键词
* @arg limit:int=20 Max number of results
*/
'use strict';
const pb = require('@phonebase-cloud/pb');
const { parseArgs } = require('node:util');
async function main() {
const { values } = parseArgs({
options: {
keyword: { type: 'string' },
limit: { type: 'string', default: '20' },
},
});
await pb.startActivity({
action: 'android.intent.action.VIEW',
data: `market://search?q=${encodeURIComponent(values.keyword)}&c=apps`,
});
await pb.sleep(1500);
return { ok: true };
}
main().catch((e) => {
console.error(e);
process.exit(1);
});Writing Guidelines
- Prefer deeplinks over simulated UI taps —
market://,mailto:, custom app schemes are 10× more reliable than clicking through a fragile UI tree. - Be idempotent — scripts should handle "already open", "not installed", "already in background" gracefully.
- Return structured data, don't print —
return { ok: true, ... }instead ofconsole.log(...). The CLI formats it for the user; HTTP/TCP callers get the object too. - Errors need context —
throw new Error(\Launching ${pkg} failed: foreground is still ${currentPkg}`)notthrow new Error('failed')`. - Never hard-code secrets — pass via
argsor usepb.secret.get(key).
4. Local Testing
Link your skill into the local pb directory and test against a real device:
# In the phonebase-skill-hub repo root
pb skills link skills/your-skill
pb device list
pb your-skill open --device <your-device-id>
pb your-skill state --device <your-device-id>
pb your-skill close --device <your-device-id>Checklist:
- [ ] Every command returns successfully
- [ ] Error cases have clear messages
- [ ] Different devices and app versions all work
5. Submit a PR
git checkout -b add-your-skill
git add skills/your-skill
git commit -m "feat: add your-skill"
git push origin add-your-skill
gh pr create --title "Add your-skill" --fillCI on PR will check:
SKILL.mdfrontmatter schema- Default export on every
scripts/*.js - JSDoc
@command/@description/@argsyntax - Basic lint rules
After merge, the docs site auto-rebuilds and your skill appears in the Skill Hub listing.
6. Checklist
Before opening a PR:
- [ ]
SKILL.mdhasname/description/package(andapp_nameif the display name differs fromname) - [ ] Every script has a JSDoc header with
@command/@description/@arg - [ ] Every script's default export is an async function returning a structured object
- [ ] Error messages include context
- [ ] Scripts are idempotent
- [ ] Tested against a real device (not only against mocks)
- [ ] No hard-coded secrets
Done? Submit a PR → phonebase-skill-hub