Building a Mini Program Skill with WeChat CloudBase

View Markdown

Last edited on 2026-08-24

This article explains how to use WeChat CloudBase as your backend to rapidly develop a Mini Program SKILL.

With CloudBase, you don't need to write wx.request, set up your own server, or maintain login state—you can connect directly to cloud functions via wx.cloud.callFunction, and the user identity OPENID is automatically injected by CloudBase's authentication-free capability.

This article assumes you are already familiar with the AI Development Mode and the four-piece set of a Mini Program SKILL (mcp.json / SKILL.md / index.js / components). It therefore focuses on one thing: how to replace "calling a self-built backend" with "calling CloudBase," and the development simplifications that come with it.

Throughout the article, a TODO list SKILL is used as the running example.


What CloudBase Saves You

In conventional SKILL development, atomic interfaces need to call a self-built backend via wx.request, and you must additionally write login middleware (wx.login → exchange for token → setStorage → server-side code2session). With CloudBase, this work can be greatly simplified:

Conventional approach Using CloudBase
wx.request to call a self-built HTTP backend wx.cloud.callFunction to call cloud functions / wx.cloud.database to connect directly to the cloud database
wx.login + token exchange + setStorage in middleware No handling required; getWXContext().OPENID retrieves it directly in the cloud function
Set up your own server, configure domains, deploy HTTPS Cloud functions + cloud database + cloud storage, ready out of the box

Prerequisite: CloudBase-related APIs are only available in the atomic interface environment (wx.cloud.init / wx.cloud.callFunction / wx.cloud.database); the atomic component environment is not yet supported. Therefore, perform all data fetching in atomic interfaces, and let components handle rendering only.


Prerequisites

  • A Mini Program AppID that has been granted "Development Mode" permission
  • WeChat DevTools Nightly build, base library version ≥ 3.16.1
  • An activated WeChat CloudBase environment, with the environment ID recorded (in the form of your-env-xxxxxx). For activation, see the WeChat CloudBase Quick Start

Step 1: Create an Independent Subpackage and Declare the SKILL

The SKILL must be placed in an independent subpackage. Configure it in app.json as follows:

{
  "lazyCodeLoading": "requiredComponents",
  "cloud": true,
  "subPackages": [
    { "root": "skills/todo-skill", "independent": true, "pages": [] }
  ],
  "agent": {
    "skills": [
      { "name": "todo", "description": "Todo list business", "path": "skills/todo-skill" }
    ]
  }
}

The directory structure is as follows (interface implementations are written directly in index.js, and components are placed uniformly under the components/ directory):

skills/todo-skill/
├── mcp.json                    # Interface and component declarations
├── SKILL.md                    # Business flow orchestration
├── index.js                    # Initialize CloudBase + implement and register all interfaces
└── components/
    └── todo-card/              # Atomic component: rendering only, no network requests
        ├── index.js
        ├── index.json
        ├── index.wxml
        └── index.wxss

Step 2: Declare Capabilities (mcp.json)

mcp.json is used to declare to the AI "which interfaces are provided, how parameters should be filled in, and which card is used for display." The way you write the description and parameter descriptions directly affects whether the AI can pick the right interface and fill in the right parameters. Be sure to clearly state the "source of values" and "how to handle missing values" in the parameter descriptions, and explicitly forbid fabrication:

{
  "apis": [
    {
      "name": "addTodo",
      "description": "Add a new todo item. Invoke when the user says 'note this down / help me record / add a todo' and provides specific content.",
      "_meta": { "ui": { "componentPath": "components/todo-card/index" } },
      "inputSchema": {
        "type": "object",
        "properties": {
          "title": {
            "type": "string",
            "description": "The todo content, taken from the user's own words (e.g. 'buy milk'). When the user has not stated specific content, do not fill it in; instead ask back 'What todo would you like to record?'."
          }
        },
        "required": ["title"]
      }
    },
    {
      "name": "listTodos",
      "description": "View the current user's entire todo list. Invoke when the user says 'show my todos / what's left to do'. No parameters required.",
      "_meta": { "ui": { "componentPath": "components/todo-card/index" } },
      "inputSchema": { "type": "object", "properties": {} }
    },
    {
      "name": "completeTodo",
      "description": "Mark a todo as completed.",
      "_meta": { "ui": { "componentPath": "components/todo-card/index" } },
      "inputSchema": {
        "type": "object",
        "properties": {
          "todoId": {
            "type": "string",
            "description": "The unique identifier of the todo. Must come from the original todoId value returned by an upstream listTodos call. Fabrication is forbidden, and inferring it from the user's natural language is forbidden; when no usable todoId exists in context, call listTodos first."
          }
        },
        "required": ["todoId"]
      }
    }
  ],
  "components": [
    { "path": "components/todo-card/index", "relatedPage": "/pages/index/index" }
  ]
}

All three interfaces share the same todo-card card for rendering; relatedPage is the page associated with the "Enter Mini Program" entry in the top-right corner of the card—fill in the corresponding todo page path in your Mini Program.


Step 3: Orchestrate the Business Flow (SKILL.md)

SKILL.md is a business-level manual for the AI, used to describe cross-interface flows, dependencies, and constraints. Do not repeat the parameter descriptions of individual interfaces from mcp.json here:

# Todo List SKILL

Helps users manage personal todos in natural language: add, view, and mark as complete.

## Business Flow

User intent
  ├─ Add ("note this down…" "help me add a todo") --> addTodo --> Todo list card
  ├─ View ("show my todos" "what's left to do") --> listTodos --> Todo list card
  └─ Complete (tap an item on the card / "mark buy milk as done") > completeTodo --> Refresh list card

## Business Constraints (Iron Rules)

- The todoId for completeTodo must come from the original value returned by listTodos; fabrication or inference
  from the user's wording is forbidden. When no usable todoId exists in context, call listTodos first and let the user
  pick from the card.
- An interface call is only considered successful when it returns isError=false; before success, announcing
  "added/completed" to the user is forbidden.
- After adding or completing, always display the latest todo list card; listing todos in plain text is forbidden.

## Intent Routing

- User provides specific content → addTodo; user only says "note this down" without content → ask back what to record first.
- User is vague (e.g. "that one") → ask for clarification first; guessing is forbidden.

Step 4: Initialize CloudBase and Implement Interfaces in index.js

index.js runs in the atomic interface environment. After initializing wx.cloud, you can implement the interface logic directly here and register them in place:

// skills/todo-skill/index.js
wx.cloud.init({ env: 'your-env-xxxxxx' })

const skill = wx.modelContext.createSkill('skills/todo-skill')

// Add todo: write operations go through a cloud function (OPENID is auto-injected by CloudBase, no login needed)
skill.registerAPI('addTodo', async ({ title }) => {
  if (!title) return { isError: true, content: [{ type: 'text', text: 'Missing todo content. Ask the user what to record; fabrication is forbidden.' }] }

  const { result } = await wx.cloud.callFunction({ name: 'addTodo', data: { title } })
  return {
    content: [{ type: 'text', text: `Added "${title}". Please display the latest todo list card.` }],
    structuredContent: { todos: result.todos }
  }
})

// View todos: read-only, connect directly to the cloud database
skill.registerAPI('listTodos', async () => {
  const { data } = await wx.cloud.database().collection('todos').orderBy('createTime', 'desc').get()
  return {
    content: [{ type: 'text', text: `${data.length} todos in total. Please display the list card; plain-text listing is forbidden.` }],
    structuredContent: { todos: data.map(t => ({ todoId: t._id, title: t.title, done: t.done })) }
  }
})

// Complete todo: write operation goes through a cloud function
skill.registerAPI('completeTodo', async ({ todoId }) => {
  if (!todoId) return { isError: true, content: [{ type: 'text', text: 'todoId must come from the listTodos return value; fabrication is forbidden.' }] }

  await wx.cloud.callFunction({ name: 'completeTodo', data: { todoId } })
  return { content: [{ type: 'text', text: 'Marked as completed. Please refresh the todo list card.' }] }
})

module.exports = skill

A few notes:

  • For content, a two-part style of "state the fact + instruct the next step" is recommended; avoid bare instructions that lack context.
  • There is no need to write wx.login login middleware anymore; if you need unified reporting or error listening, you can use skill.use(...).
  • listTodos is a read-only scenario, so connecting directly to the cloud database is fine; write operations (addTodo / completeTodo) must all be validated through cloud functions—see Security Essentials.

Step 5: Authentication—Delegated to CloudBase's Authentication-Free Capability

An important capability of CloudBase is that when you call a cloud function, WeChat automatically injects the current user identity into the cloud function context. The Mini Program side does not need to log in, nor does it need to pass openid.

  • Mini Program side: call wx.cloud.callFunction directly, without (and you should not) passing openid.
  • Cloud function side: retrieve the current user identity via cloud.getWXContext().OPENID.

Note: The parameters generated by the AI cannot be trusted. The authentication-free capability solves "who the user is," but "whether a given todoId belongs to that user" still requires ownership / authorization checks in the cloud function (see Security Essentials).


Step 6: Write Cloud Functions (Your Backend Logic)

Cloud functions are standard CloudBase cloud functions; the SKILL does not interfere with them at all. In a cloud function, the user identity can be obtained directly, and the database can be read and written directly:

// cloudfunctions/addTodo/index.js
const cloud = require('wx-server-sdk')
cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV })
const db = cloud.database()

exports.main = async (event) => {
  const { OPENID } = cloud.getWXContext()
  const title = String(event.title || '').slice(0, 100)   // Server-side re-validation
  if (!title) return { code: 1, msg: 'Content is empty' }

  await db.collection('todos').add({ data: { _openid: OPENID, title, done: false, createTime: db.serverDate() } })
  const { data } = await db.collection('todos').where({ _openid: OPENID }).orderBy('createTime', 'desc').get()
  return { code: 0, todos: data.map(t => ({ todoId: t._id, title: t.title, done: t.done })) }
}
// cloudfunctions/completeTodo/index.js
const cloud = require('wx-server-sdk')
cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV })
const db = cloud.database()

exports.main = async (event) => {
  const { OPENID } = cloud.getWXContext()
  // Include _openid in the where clause to ensure users can only modify their own todos (authorization check)
  await db.collection('todos').where({ _id: event.todoId, _openid: OPENID }).update({ data: { done: true } })
  return { code: 0 }
}

For cloud function dependency management and deployment, see the Cloud Functions documentation.


Step 7: Write the Atomic Component (Rendering Only)

The component environment does not support wx.cloud; all its data comes from the structuredContent returned by interfaces. When the user taps a todo, completeTodo is triggered via an on-behalf-of-user upstream message:

// skills/todo-skill/components/todo-card/index.js
Component({
  data: { todos: [] },
  lifetimes: {
    created() {
      this.ctx = wx.modelContext.getContext(this)
      this.ctx.on(wx.modelContext.NotificationType.Result, ({ result }) => {
        this.setData({ todos: (result.structuredContent || {}).todos || [] })
      })
    }
  },
  methods: {
    onTapTodo(e) {
      const { todoId, title } = e.currentTarget.dataset.todo
      this.ctx.sendFollowUpMessage({
        content: [
          { type: 'text', text: `Complete "${title}"` },
          { type: 'api/call', data: { name: 'completeTodo', arguments: { todoId } } }
        ]
      })
    }
  }
})
<!-- skills/todo-skill/components/todo-card/index.wxml -->
<view class="card">
  <view wx:for="{{todos}}" wx:key="todoId" class="item {{item.done ? 'done' : ''}}"
        data-todo="{{item}}" bindtap="onTapTodo">
    {{item.done ? '[x]' : '[ ]'}} {{item.title}}
  </view>
</view>

skills/todo-skill/components/todo-card/index.json:

{ "component": true }

Security Essentials

Security is the most critical part in CloudBase scenarios. Please implement each item carefully.

  1. AI parameters cannot be trusted: Both atomic interfaces and cloud functions must validate the type, range, and validity of parameters (for example, whether title is non-empty and whether it exceeds the length limit).
  2. Perform ownership / authorization checks in cloud functions: Authentication-free capability only solves "who the user is." Before performing operations, use OPENID for comparison (include _openid in the where clause) to avoid mistakenly modifying others' data.
  3. All write operations go through cloud functions: Add, delete, and update must all be persisted by cloud functions; the client is forbidden from connecting directly to the database for writes.
  4. Read-only direct connections require accompanying security rules: For client-side direct queries via wx.cloud.database() like listTodos, you must restrict auth.openid == doc._openid in the Security Rules; otherwise, others' todos may be read.
  5. Secrets stored only in environment variables: Sensitive information such as third-party keys should be stored in cloud function environment variables; never write them into Mini Program client code or interface return values.

A Complete Data Flow

The following uses "help me record a todo: buy milk" as an example to connect the various roles involved in this article:

sequenceDiagram participant U as WeChat User participant AI as WeChat AI Backend participant IDX as Skills Atomic Interface (skills/todo-skill/index.js) participant CF as WeChat CloudBase - Cloud Function participant DB as WeChat CloudBase - Cloud Database participant CMP as Skills Atomic Component (skills/todo-skill/components) U->>AI: "help me record a todo: buy milk" AI->>IDX: call addTodo(title="buy milk") IDX->>CF: wx.cloud.callFunction('addTodo') CF->>CF: getWXContext().OPENID to get identity CF->>DB: write to todos collection DB-->>CF: return latest todo list CF-->>IDX: return result IDX->>AI: assemble { content, structuredContent } and return AI->>CMP: dispatch render instruction CMP->>U: todo-card renders the todo list card Note over U,DB: Update todo (user interaction flow) U->>CMP: tap a todo CMP->>CF: sendFollowUpMessage triggers completeTodo CF->>CF: verify _openid identity CF->>DB: update todo status DB-->>CF: return latest list CF-->>CMP: return result CMP->>U: refresh todo card

At this point, you only need to write code in three places: the atomic interface (call cloud functions or connect directly to the database in index.js), the cloud functions (business logic), and the atomic component (rendering).

Login state, server, and domain configuration are all taken care of—WeChat CloudBase handles them for you.