PhoenixDevelopers

Hello Jibo: complete example

On this page

A working two-card touchscreen skill with speech, navigation and close. Download it, run its tests, then change the text and submit your own version.

Download starter ZIP · Skill SDK walkthrough · API reference

Who this is for: developers who want a complete, runnable first skill with no dependency install.

Run it on your computer

Unzip the download and open the hello-jibo folder in a terminal. Node.js 20 or later is required. No dependency install, account or network service is needed:

node --test test.mjs
node index.js

The test exercises greeting, both cards and close. The preview prints events; it does not speak through a robot. package.json in the download only selects JavaScript modules for the computer preview.

Pack and submit

Create your own app identity in Publish, copy its exact app ID into skill.json, then use the kit from a Phoenix source checkout:

node tools/skill-kit/cli.mjs check ./hello-jibo
node tools/skill-kit/cli.mjs test ./hello-jibo
node tools/skill-kit/cli.mjs pack ./hello-jibo

Choose hello-jibo.phskill under your app's Skill bundle picker, or ZIP the folder and let the browser pack it. Review attachments are retained as text; only skill.json becomes robot data. Submission means pending review, not public installation.

The source below is read from the same files used by the kit and ZIP download. It is copyable text, never evaluated by this page.

skill.json

{
  "schema": 1,
  "runtime": "phoenix-declarative-v1",
  "appId": "app-00000000-0000-4000-8000-000000000000",
  "version": "1.0.0",
  "name": "Hello Jibo",
  "description": "A small touchscreen greeting with optional speech.",
  "minFirmware": "13.5.3",
  "permissions": [
    "display",
    "speech"
  ],
  "launchNames": [
    "hello friend"
  ],
  "examples": [
    "Open Hello Jibo, then tap Say hello."
  ],
  "releaseNotes": "First version.",
  "startPage": "welcome",
  "pages": [
    {
      "id": "welcome",
      "title": "Hello Jibo",
      "text": "A little hello, made by you.",
      "buttons": [
        {
          "label": "Say hello",
          "action": {
            "type": "speak",
            "text": "Hello, friend. Thanks for trying my first skill."
          }
        },
        {
          "label": "More",
          "action": {
            "type": "show",
            "page": "about"
          }
        },
        {
          "label": "Close",
          "action": {
            "type": "close"
          }
        }
      ]
    },
    {
      "id": "about",
      "title": "Make it yours",
      "text": "Change this text in skill.json, then check and pack again.",
      "buttons": [
        {
          "label": "Back",
          "action": {
            "type": "show",
            "page": "welcome"
          }
        }
      ]
    }
  ]
}

index.js

// Computer-only preview. The robot installs skill.json data, not this code.
import { readFileSync } from 'node:fs';
import { pathToFileURL } from 'node:url';

export function createPreview(skill) {
  let current = null;
  const page = () => skill.pages.find(p => p.id === current) || null;
  return {
    start() { current = skill.startPage; return page(); },
    tap(index) {
      const button = page()?.buttons[index];
      if (!button) throw new Error('Choose a button on the current page.');
      const action = button.action;
      if (action.type === 'show') current = action.page;
      else if (action.type === 'close') current = null;
      else if (action.type === 'speak') {
        if (!skill.permissions.includes('speech')) throw new Error('Speech permission is required.');
        return { speech: action.text, page: page() };
      } else throw new Error('Unsupported action.');
      return { speech: null, page: page() };
    },
    stop() { current = null; return null; }
  };
}

if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
  const skill = JSON.parse(readFileSync(new URL('./skill.json', import.meta.url), 'utf8'));
  const preview = createPreview(skill);
  console.log(preview.start());
  console.log(preview.tap(0));
  preview.stop();
}

test.mjs

import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { createPreview } from './index.js';
const skill = JSON.parse(readFileSync(new URL('./skill.json', import.meta.url), 'utf8'));

test('greeting, page navigation and close work in the computer preview', () => {
  const preview = createPreview(skill);
  assert.equal(preview.start().id, 'welcome');
  assert.match(preview.tap(0).speech, /Hello, friend/);
  assert.equal(preview.tap(1).page.id, 'about');
  assert.equal(preview.tap(0).page.id, 'welcome');
  assert.equal(preview.tap(2).page, null);
  assert.equal(preview.stop(), null);
});