How-to guide
SDK and CLI recipes
Copyable recipes for common jobs, each with the SDK, the CLI and HTTP side by side.
For Developers and automation owners
On this page
App links open your signed-in workspace. Check the selected workspace before making changes.
Before you start
Every recipe uses a workspace API key with the permissions it names, and nothing more. List company members is the exception: it uses a company key.
The SDK examples start from the client below. The CLI and HTTP examples read the same two environment variables.
Writes send a requestKey. Use a new one for each thing you mean to do, and the same one only to retry it. A retry with the same key returns the first receipt and doesn’t do the work again.
import { Shuffl } from '@shuffl/sdk';
const shuffl = new Shuffl({ apiKey: process.env.SHUFFL_API_KEY! });Invite people from a CSV
Read the email column of a CSV and invite everyone in it to the workspace as members. Each person gets an email invitation. People already invited or already in the workspace are counted, not invited twice.
Use one request key per batch. If the call is cut off, send the same batch with the same key: you get the first receipt back and nobody is invited twice.
Send a large file in batches, each with its own key.
Permissions: invitations.createMany.
import { readFile } from 'node:fs/promises';
const [header = '', ...rows] = (await readFile('people.csv', 'utf8'))
.trim()
.split('\n');
const column = header.split(',').indexOf('email');
const emails = rows
.map((row) => row.split(',')[column]?.trim() ?? '')
.filter((email) => email.includes('@'));
const { receipt, result } = await shuffl.invitations.createMany({
emails,
role: 'member',
confirm: 'invitations.createMany',
requestKey: 'invite-november-hires',
});
console.log(receipt.status, result?.invited.length, result?.alreadyMember);Publish a Knowledge article
Save a Knowledge article as a draft, then publish it to its audience. A new article needs a new UUID as its sourceId and expectedRevision 0. Publishing takes the version id and revision the save returned.
Publishing is what makes the text answerable: Mello and search_knowledge only use published sources. The key’s owner must be allowed to publish Knowledge.
Set reviewAt to when someone should check the article again. After that date it stops being used until it is reviewed.
Permissions: knowledge.sources.save, knowledge.sources.publish.
const saved = await shuffl.knowledge.sources.save({
sourceId: crypto.randomUUID(),
expectedRevision: 0,
title: 'Parental leave',
body: 'Write the policy here. Once it is published, Mello answers from this text and cites it.',
audience: 'everyone',
effectiveAt: new Date().toISOString(),
reviewAt: '2027-05-01T00:00:00.000Z',
requestKey: 'parental-leave-draft',
});
// A repeated request key returns result: null. Read the source instead.
if (!saved.result) throw new Error('Already saved: read it with knowledge.sources.get');
await shuffl.knowledge.sources.publish({
sourceId: saved.result.source.id,
versionId: saved.result.version.id,
expectedRevision: saved.result.source.revision,
confirm: 'knowledge.sources.publish',
requestKey: 'parental-leave-publish',
});Create a Connections program
Pick a Slack channel, create a Connections program for its members, then turn it on. channels.list shows each channel’s readiness: use one that says ready. The program starts paused, so nothing is sent until you resume it.
Resuming turns on the program’s schedule. To see who would meet before you do, call programs.preview first.
Permissions: channels.list, programs.create, programs.resume.
const channels = await shuffl.channels.list();
const channel = channels.find((entry) => entry.name === 'coffee-chats');
if (channel?.readiness !== 'ready') throw new Error('Add Shuffl to the channel first');
const created = await shuffl.programs.create({
draft: true,
requestKey: 'coffee-chats-create',
program: {
name: 'Coffee chats',
channelId: channel.id,
channelName: channel.name,
includeBios: true,
intro: 'Meet someone new this fortnight. Pick a time that suits you both.',
match: { strategy: 'new-connections', groupSize: 2, maxGroupSize: 3, iterations: 50 },
schedule: {
frequency: 'weeks',
interval: 2,
anchor: '2026-11-02T09:00:00',
timezone: 'America/New_York',
},
},
});
await shuffl.programs.resume({ id: created.id });Schedule Pulse
Read the Pulse settings, change when questions go out, and turn Pulse on. sendDays runs from 1 for Monday to 7 for Sunday. sendHour and sendMinute are in the timezone you give.
expectedRevision is the revision you read, so you never overwrite a change someone made since.
Setting status to active schedules questions to everyone in the workspace. Answers are only shown for groups of reportingFloor people or more, and never one person’s answer.
Permissions: pulse.program, pulse.saveProgram.
const current = await shuffl.pulse.program({});
await shuffl.pulse.saveProgram({
...current.settings,
sendDays: [2],
sendHour: 10,
sendMinute: 0,
timezone: 'America/New_York',
status: 'active',
expectedRevision: current.revision,
confirm: 'pulse.saveProgram',
requestKey: 'pulse-tuesdays',
});Give praise or a badge
Praise is posted as the key’s owner in a praise channel. Badges are given by someone who owns the badge.
praise.composer lists the channels, the people you can praise and your company values, or says why praise is unavailable. Pick recipients from its people.
composerKey identifies one praise message, so a retry with the same key never posts it twice. Use 8 to 64 letters, digits, hyphens or underscores. badges.list marks the badges you can give with canAward.
Permissions: praise.composer, praise.send, badges.list, badges.award.
const composer = await shuffl.praise.composer({});
if (composer.blocked) throw new Error('Praise is unavailable: ' + composer.blocked);
await shuffl.praise.send({
channelId: composer.channels[0]!.id,
recipientIds: ['<person id>'],
text: 'Thank you for running the onboarding session this week.',
composerKey: 'thanks-onboarding-session',
confirm: 'praise.send',
requestKey: 'thanks-onboarding-session',
});
const badges = await shuffl.badges.list({});
const mentor = badges.find((badge) => badge.canAward && badge.name === 'Mentor');
if (mentor)
await shuffl.badges.award({
badgeId: mentor.id,
personId: '<person id>',
reason: 'Mentored two new hires this quarter.',
confirm: 'badges.award',
requestKey: 'mentor-badge-maya',
});Read Pulse insights
Read Pulse participation, favourable scores and trends for the last 30, 90 or 180 days. Results only cover groups at or above the reporting floor, which is never below seven people, and no operation returns one person’s answer.
withheld counts the questions whose results stay hidden because too few people answered. Use pulse.questionDetail for one closed question and pulse.actions for what the company is doing about it.
Permissions: pulse.insights.
const insights = await shuffl.pulse.insights({ days: 90 });
console.log(insights.participationPct, insights.favourablePct, insights.withheld);List open HR requests
List the HR requests that still need someone: every request whose status is open, waiting_on_hr or waiting_on_employee.
hr.list returns the requests the key’s owner asked or can answer. An HR responder sees their queue and an employee sees their own.
HR requests are confidential. Keep the output out of shared logs and chat. dueAt is when the response target runs out.
Permissions: hr.list.
const requests = await shuffl.hr.list({});
const open = requests.filter((request) => request.status !== 'resolved');
for (const request of open)
console.log(request.id, request.status, request.category?.name, request.dueAt);Export people
Page through the People directory and write it to a CSV. Each page holds up to 100 people; pass nextCursor back as cursor until it comes back null.
For a full copy of the workspace, with programs and introduction history as well as people, an administrator can use exports.team.
Permissions: users.list.
import { writeFile } from 'node:fs/promises';
const people = [];
let cursor: string | undefined;
do {
const page = await shuffl.users.list({ limit: 100, cursor });
people.push(...page.items);
cursor = page.nextCursor ?? undefined;
} while (cursor);
const cell = (value: string | null) => JSON.stringify(value ?? '');
await writeFile(
'people.csv',
['id,name,email', ...people.map((p) => [p.id, p.name, p.email].map(cell).join(','))].join('\n'),
);List company members
List everyone in the company, across all its workspaces, with their company role.
This recipe takes a company key instead of a workspace key. A company owner or administrator creates one in Settings → API keys and webhooks → Company keys with the company.members.list permission.
In the SDK, pass the company key to new Shuffl and call shuffl.company. In the CLI, save it with shuffl login --company or set SHUFFL_COMPANY_KEY. To offboard someone, company.members.remove takes a userId from this list.
Permissions: company.members.list.
import { Shuffl } from '@shuffl/sdk';
const shuffl = new Shuffl({ apiKey: process.env.SHUFFL_COMPANY_KEY! });
const members = [];
let cursor: string | undefined;
do {
const page = await shuffl.company.members.list({ limit: 100, cursor });
members.push(...page.items);
cursor = page.nextCursor ?? undefined;
} while (cursor);
for (const member of members)
console.log(member.userId, member.role, member.email);Find another operation
Every operation has the same three forms. The REST API reference shows each one’s fields with an SDK call, a CLI command and a curl example.