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-cardcard for rendering;relatedPageis 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.loginlogin middleware anymore; if you need unified reporting or error listening, you can useskill.use(...). listTodosis 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.callFunctiondirectly, without (and you should not) passingopenid. - 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.
- AI parameters cannot be trusted: Both atomic interfaces and cloud functions must validate the type, range, and validity of parameters (for example, whether
titleis non-empty and whether it exceeds the length limit). - Perform ownership / authorization checks in cloud functions: Authentication-free capability only solves "who the user is." Before performing operations, use
OPENIDfor comparison (include_openidin thewhereclause) to avoid mistakenly modifying others' data. - 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.
- Read-only direct connections require accompanying security rules: For client-side direct queries via
wx.cloud.database()likelistTodos, you must restrictauth.openid == doc._openidin the Security Rules; otherwise, others' todos may be read. - 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:
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.