Configuring Horizon
coollabsio/coolify
A skill your agent uses whenever the user mentions Horizon by name in a Laravel context.
Manage contacts, communication channels, access control, and invite links
$ npx skills add vellum-ai/vellum-assistant --skill contacts -a claude-codeProject install by default; add -g for ~/.claude/skills/.
$ gh skill install vellum-ai/vellum-assistant contacts --agent claude-codeProject scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).
$ git clone --depth 1 https://github.com/vellum-ai/vellum-assistant.git skills-src && mkdir -p .claude/skills && cp -r skills-src/assistant/src/config/bundled-skills/contacts .claude/skills/contacts && rm -rf skills-srcUse ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.
Claude Code skills documentation · loads skills from .claude/skills/
Install the "contacts" agent skill from https://github.com/vellum-ai/vellum-assistant/tree/main/assistant/src/config/bundled-skills/contacts into .claude/skills/contacts/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "contacts", then confirm the skill loads.Claude Code copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$skill-installer install https://github.com/vellum-ai/vellum-assistant/tree/main/assistant/src/config/bundled-skills/contactsType this inside Codex. $skill-installer <name> installs a curated skill from openai/skills. The installer writes to $CODEX_HOME/skills (default ~/.codex/skills). Restart Codex if the skill does not show up.
$ npx skills add vellum-ai/vellum-assistant --skill contacts -a codexProject install goes to .agents/skills/; add -g for ~/.codex/skills/.
$ gh skill install vellum-ai/vellum-assistant contacts --agent codexProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/vellum-ai/vellum-assistant.git skills-src && mkdir -p .agents/skills && cp -r skills-src/assistant/src/config/bundled-skills/contacts .agents/skills/contacts && rm -rf skills-srcUse ~/.agents/skills/ instead of .agents/skills for a personal install.
Codex skills documentation · loads skills from .agents/skills/
Install the "contacts" agent skill from https://github.com/vellum-ai/vellum-assistant/tree/main/assistant/src/config/bundled-skills/contacts into .agents/skills/contacts/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "contacts", then confirm the skill loads.Codex copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add vellum-ai/vellum-assistant --skill contacts -a cursorProject install goes to .agents/skills/; add -g for ~/.cursor/skills/.
$ gh skill install vellum-ai/vellum-assistant contacts --agent cursorProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/vellum-ai/vellum-assistant.git skills-src && mkdir -p .cursor/skills && cp -r skills-src/assistant/src/config/bundled-skills/contacts .cursor/skills/contacts && rm -rf skills-srcUse ~/.cursor/skills/ instead of .cursor/skills for a personal install.
Cursor skills documentation · loads skills from .cursor/skills/, .agents/skills/, .claude/skills/, .codex/skills/
Install the "contacts" agent skill from https://github.com/vellum-ai/vellum-assistant/tree/main/assistant/src/config/bundled-skills/contacts into .cursor/skills/contacts/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "contacts", then confirm the skill loads.Cursor copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gemini skills install https://github.com/vellum-ai/vellum-assistant.git --path assistant/src/config/bundled-skills/contacts--scope user (default) or --scope workspace; --path is the subfolder of the repo that holds the skill; --consent skips the security confirmation prompt.
$ npx skills add vellum-ai/vellum-assistant --skill contacts -a gemini-cliProject install goes to .agents/skills/; add -g for ~/.gemini/skills/.
$ gh skill install vellum-ai/vellum-assistant contacts --agent gemini-cliProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/vellum-ai/vellum-assistant.git skills-src && mkdir -p .gemini/skills && cp -r skills-src/assistant/src/config/bundled-skills/contacts .gemini/skills/contacts && rm -rf skills-srcUse ~/.gemini/skills/ instead of .gemini/skills for a personal install, then run /skills reload.
Gemini CLI skills documentation · loads skills from .gemini/skills/, .agents/skills/
Install the "contacts" agent skill from https://github.com/vellum-ai/vellum-assistant/tree/main/assistant/src/config/bundled-skills/contacts into .gemini/skills/contacts/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "contacts", then confirm the skill loads.Gemini CLI copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gh skill install vellum-ai/vellum-assistant contactsInstalls for Copilot at project scope by default; add --scope user for a personal install. Preview a skill first with gh skill preview. Needs GitHub CLI 2.90.0 or later (public preview).
$ npx skills add vellum-ai/vellum-assistant --skill contacts -a github-copilotProject install goes to .agents/skills/; add -g for ~/.copilot/skills/.
$ git clone --depth 1 https://github.com/vellum-ai/vellum-assistant.git skills-src && mkdir -p .github/skills && cp -r skills-src/assistant/src/config/bundled-skills/contacts .github/skills/contacts && rm -rf skills-srcUse ~/.copilot/skills/ instead of .github/skills for a personal install. Commit .github/skills so cloud agent and code review can use it.
GitHub Copilot skills documentation · loads skills from .github/skills/, .claude/skills/, .agents/skills/
Install the "contacts" agent skill from https://github.com/vellum-ai/vellum-assistant/tree/main/assistant/src/config/bundled-skills/contacts into .github/skills/contacts/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "contacts", then confirm the skill loads.GitHub Copilot copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add vellum-ai/vellum-assistant --skill contacts -a opencodeOpenCode documents no install command of its own. Project install goes to .agents/skills/; add -g for ~/.config/opencode/skills/.
$ gh skill install vellum-ai/vellum-assistant contacts --agent opencodeProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/vellum-ai/vellum-assistant.git skills-src && mkdir -p .opencode/skills && cp -r skills-src/assistant/src/config/bundled-skills/contacts .opencode/skills/contacts && rm -rf skills-srcUse ~/.config/opencode/skills/ instead of .opencode/skills for a personal install.
OpenCode skills documentation · loads skills from .opencode/skills/, .claude/skills/, .agents/skills/
Install the "contacts" agent skill from https://github.com/vellum-ai/vellum-assistant/tree/main/assistant/src/config/bundled-skills/contacts into .opencode/skills/contacts/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "contacts", then confirm the skill loads.OpenCode copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
contactsManage contacts, communication channels, access control, and invite links
Contacts is an agent skill from vellum-ai/vellum-assistant. Manage contacts, communication channels, access control, and invite links
Its SKILL.md is about 8.9k tokens, which your agent loads only when the skill is triggered. The skill folder holds 5 other files (for example `TOOLS.json`, `tools/contact-merge.ts` and `tools/contact-search.ts`). Compatibility notes: Designed for Vellum personal assistants
It sits in Backend & APIs, covering Authorization and RBAC. The repository describes itself as: An AI Assistant that’s easy to setup, does your work 24/7, knows your preferences and gets better over time. The licence is MIT.
3 steps, taken from the first numbered list in SKILL.md.
Read from SKILL.md and the folder at commit 33cc983. It shows what the files ask for, not the result of running them.
Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.
From allowed-tools in the SKILL.md frontmatter.
Ships script files (TypeScript), which the agent can run.
Shell commands in SKILL.md call:
python3From the folder's file list and the shell code blocks in SKILL.md.
Hosts in commands or code, which the agent is likely to contact:
t.meFrom URLs in SKILL.md, links to its own repository left out.
Names these keys or tokens, usually read from environment variables:
INVITE_TOKENFrom names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.
Designed for Vellum personal assistants
From compatibility in the SKILL.md frontmatter.
Contacts loads about 8.9k tokens when it runs. Until then it costs about 21 tokens; SKILL.md has 4,666 words of instructions outside code blocks.
Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.
The automated check found no risky patterns in SKILL.md.
Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.
The full file from vellum-ai/vellum-assistant at commit 33cc983, republished under its MIT licence (© vellum-ai). 4,666 words, ~8,950 tokens.
.claude/skills/contacts/SKILL.md (or your agent's skills folder). This skill also uses 4 other files; get the full folder from GitHub.Manage the user's contacts, relationship graph, access control (trusted contacts), and invite links. This skill covers contact CRUD with multi-channel tracking, controlling who can message the assistant through external channels (Telegram, phone), and creating/managing invite links that grant access.
Every contact write goes through the guardian. You can list and search contacts on your own. To create, edit, delete, merge, or bind a channel to one, run the matching command below: each opens a form in the guardian's app, and the write happens only if they submit it. You are proposing, not writing. Say so: "I'll put that up for you to confirm," not "Done."
Two things a contact record does not do: it grants no access (a record has no channels, so nobody can message the assistant through it), and it is not a channel. Use
assistant contacts channels add <contact_id>to bind an address to a contact that already exists, orassistant contacts invites createto let someone bind their own.The form needs the guardian's desktop or web app to be open. If a command reports that it timed out, nobody answered: tell the user, and do not retry in a loop.
One exception you must not take: the
contact_mergetool writes immediately, with no confirmation. Useassistant contacts mergeinstead, so the guardian sees what is being combined before the donor record is deleted.A form the guardian closes without answering is not a failure. The command prints
Cancelled: nothing was written, exits 130, and writes nothing. Report that as "the guardian did not confirm", not as an error, and do not retry in a loop.
assistant contacts create --name "Alice" --jsonOpens an add-contact form prefilled with what you pass. The guardian can edit the name and notes before submitting, so treat the result as theirs, not yours: read the command's output rather than assuming the values you proposed were the ones written.
Optional flags:
--notes -- proposed notes, prefilled into the form--channel -- also bind a channel on the same form (email, phone, telegram, whatsapp, slack). Requires --name.--address -- address to pre-fill for that channel; the guardian can edit it before submitting. Requires --channel.--verify -- pre-check the form's "mark verified" box. Requires --channel.--label / --description -- what the form says about why you are asking--timeout -- how long the form stays open, in ms (default 300000)With --channel, the record and the channel are one form and one guardian confirmation:
assistant contacts create --name "Alice" --channel email --address "alice@example.com" --json--name is required in that mode, because the contact is created under it. The form shows --notes but does not let the guardian edit them, so they are written as passed; use assistant contacts update to change them afterwards. --address and --verify each need --channel, so either one without it is refused rather than ignored.
The --json shape follows the mode: without --channel it is {ok, contact}, so the new id is contact.id; with --channel it is the address form's {ok, contactId, channelId, channelType, address, verified}, so the id is contactId. Read the right one before passing it to contacts invites create --contact-id.
Use this before creating an invite for someone who is not in the contact graph yet.
assistant contacts channels add "<contact_id>" --channel email --address "alice@example.com" --jsonOpens an address form naming that contact, so the guardian can see where the channel is going. Whatever address they submit binds to that contact: one confirmation, and no second record. This is how you add an address to somebody who is already in the graph. --channel is required.
Optional flags:
--address -- address to pre-fill; the guardian can edit it before submitting--verify -- pre-check the form's "mark verified" box. The guardian decides: unchecked, the channel stays unverified and cannot message the assistant.--label / --description -- what the form says about why you are asking--timeout -- how long the form stays open, in ms (default 300000)Two contacts cannot share one address. An address held by a different contact when the form is submitted is refused, the refusal names that contact, and nothing is written. Passing --address also checks up front and warns when the address looks taken, without refusing.
For an address that belongs to nobody yet, assistant contacts prompt is the create-or-bind path:
assistant contacts prompt --channel email --json
assistant contacts prompt --contact-id "<contact_id>" --channel email --jsonOn its own it looks up the (channel type, address) pair the guardian submits: a match reuses that contact and channel, and no match creates a new contact named after the address. --contact-id targets an existing contact instead, the same bind channels add does under its own verb.
assistant contacts update "<contact_id>" --name "Alice Chen" --json
assistant contacts update "<contact_id>" --notes "Moved to Berlin" --jsonOpens an edit form showing the current name next to the proposed change. At least one of --name or --notes is required. This edits the record only. To revoke or block someone's access, use assistant contacts channels update-status.
assistant contacts delete "<contact_id>" --jsonOpens a confirmation showing the contact and its channels. Deleting a contact deletes its channels too, so anyone reaching the assistant through them loses access. A guardian contact cannot be deleted. When two records are the same person, run assistant contacts merge <surviving_contact_id> <donor_contact_id> instead: the donor's channels move to the survivor rather than being destroyed with it.
Search for contacts by name, channel address, or other criteria.
assistant contacts list --query "<search_term>" --jsonOptional flags:
--query -- search by display name (partial match)--channel-address -- search by channel address (email, phone, handle)--channel-type -- filter by channel type when searching by address--limit -- maximum results to return (default 50, max 100)When you discover two contacts are the same person (e.g. same person on email and Slack), merge them to consolidate.
assistant contacts merge <surviving_contact_id> <donor_contact_id> --jsonOpens a confirmation naming both contacts and listing the channels that move. Nothing is merged unless the guardian submits it. On submit:
A contact's interaction count is the sum over its channels, so a moved channel brings its history with it. An address the survivor already holds is left where it is, and its donor-side history goes with the deleted record.
Nobody loses access: every address that reached the donor reaches the survivor afterwards. A guardian contact cannot be the donor, so keep the guardian as the survivor.
The survivor keeps its own name unless the guardian edits it on the form. --keep-donor-name seeds that field with the donor's name, for when the donor is the better-named record of the two.
Trusted contacts control who is allowed to send messages to the assistant through external channels like Telegram and voice (phone calls).
channels array. Each channel entry has its own status and policy.allow (can message freely) or deny (blocked from messaging).active (currently effective), revoked (access removed), or blocked (explicitly denied).telegram, phone).Use this to show the user who currently has access, or to look up a specific contact.
assistant contacts list --jsonOptional query parameters for filtering:
--role <role> -- filter by role (default: contact; use guardian to list guardians)--limit <limit> -- maximum number of contacts to return--query <query> -- search query to filter contactsExample:
assistant contacts list --role contact --jsonThe response contains { ok: true, contacts: [...] } where each contact has:
id -- unique contact IDrole -- the contact's role (contact, guardian)displayName -- human-readable namechannels -- array of channel entries, each with:id -- channel ID (needed for status/policy changes)channel -- the channel type (e.g., telegram, phone)externalUserId -- the user's ID on that channelexternalChatId -- the chat ID on that channeldisplayName -- channel-specific display nameusername -- channel username (e.g., Telegram @handle)status -- current status (active, revoked, blocked, etc.)policy -- current policy (allow, deny)createdAt -- when the contact was addedPresenting results: Format the contact list as a readable table or list. Include display name, role, and per-channel status/policy. If no contacts exist, tell the user their contact list is empty.
Access is granted per channel, not per contact, and it always takes proof that the person holds the address. Creating the contact record is only the first step.
assistant contacts create --name "<name>". The guardian confirms it. This alone grants nothing.assistant contacts invites create --source-channel <channel> --contact-id <contact_id>. The invitee redeems it from the address they actually control, which is what makes the channel trusted. This is the path to prefer: it works even when nobody knows the invitee's handle on that channel up front.assistant contacts channels add <contact_id> --channel <channel> opens a form naming that contact and binds the address there. For a brand new address with no record behind it, assistant contacts prompt --channel <channel> creates the contact from what the guardian types. Either way the channel lands unverified unless the guardian checks the verify box on that form, and an unverified channel cannot message the assistant.Only the guardian's own submission can complete any of these, so never promise access is live until the command returns.
Use this when the user wants to remove someone's access. Always confirm with the user before executing this action.
Ask the user: "I'll revoke access for [name/identifier]. They will no longer be able to message the assistant. Should I proceed?"
First, list contacts to find the channel's id (each entry in a contact's channels array has an id field -- visible in assistant contacts list --json output), then revoke:
Important: Before revoking, check the channel's current status. If the channel is blocked, do not attempt to revoke it -- blocking is stronger than revoking. Inform the user that the contact is already blocked and revoking is not applicable. Only channels with active or pending status can be revoked.
assistant contacts channels update-status <channel_id> --status revoked --reason "<optional reason>" --jsonReplace <channel_id> with the channel's id from the contact's channels array.
Use this when the user wants to explicitly block someone. Blocking is stronger than revoking -- it marks the contact as actively denied. Always confirm with the user before executing this action.
Ask the user: "I'll block [name/identifier]. They will be permanently denied from messaging the assistant. Should I proceed?"
assistant contacts channels update-status <channel_id> --status blocked --reason "<optional reason>" --jsonReplace <channel_id> with the channel's id from the contact's channels array (visible in assistant contacts list --json output).
Invite links let the guardian share a link or code that automatically grants access when used. Telegram invites use a deep link; voice invites use a phone number + numeric code; email, WhatsApp, Slack, and Discord invites use a 6-digit code that the invitee sends to the assistant on the respective channel. Discord invitees must share a server with the bot and have DMs from server members enabled before they can send it the code.
Every invite must be bound to a contact. Before creating an invite, look up the contact with assistant contacts list and pass the contact's id via the required --contact-id flag. If the target contact doesn't exist yet, create it first with assistant contacts create --name "<name>" and wait for the guardian to confirm; the invite needs only a name, never the invitee's address on that channel.
Use this when the guardian wants to invite someone to message the assistant on Telegram without needing their user ID upfront. The invite link is a shareable Telegram deep link -- when someone opens it, they automatically get trusted-contact access.
Important: The shell snippet below emits a <vellum-sensitive-output> directive containing the raw invite token. The tool executor automatically strips this directive and replaces the raw token with a placeholder so the LLM never sees it. The placeholder is resolved back to the real token in the final assistant reply.
INVITE_JSON=$(assistant contacts invites create --source-channel telegram --contact-id "<contact_id>" --max-uses 1 --note "<optional note, e.g. the person it is for>" --json)
INVITE_TOKEN=$(printf '%s' "$INVITE_JSON" | python3 -c "
import json, sys
data = json.load(sys.stdin)
invite = data.get('invite', {})
print(invite.get('token', ''), end='')
")
INVITE_URL=$(printf '%s' "$INVITE_JSON" | python3 -c "
import json, sys
data = json.load(sys.stdin)
invite = data.get('invite', {})
share = invite.get('share') or {}
print(share.get('url', ''), end='')
")
if [ -z "$INVITE_TOKEN" ]; then
printf '%s\n' "$INVITE_JSON"
exit 1
fi
# Prefer backend-provided canonical link when available.
if [ -z "$INVITE_URL" ]; then
BOT_USERNAME=$(assistant config get telegram.botUsername)
if [ -z "$BOT_USERNAME" ] || [ "$BOT_USERNAME" = "(not set)" ]; then
echo "error:no_share_url_or_bot_username"
exit 1
fi
INVITE_URL="https://t.me/$BOT_USERNAME?start=iv_$INVITE_TOKEN"
fi
echo "<vellum-sensitive-output kind=\"invite_code\" value=\"$INVITE_TOKEN\" />"
echo "$INVITE_URL"Required flags:
--source-channel -- must be telegram--contact-id -- the ID of the contact this invite is for. Look up the contact first with assistant contacts list, or create one with assistant contacts create if they are not in the graph yet.Optional flags:
--max-uses -- how many times the link can be used (default: 1). Use a higher number for group invites.--expires-in-ms -- expiration time in milliseconds from now (e.g., 86400000 for 24 hours). Defaults to 7 days (604800000) if omitted.--note -- a human-readable label for the invite (e.g., "For Mom", "Family group").The create response contains { ok: true, invite: { id, token, share?, ... } }.
token is the raw invite token and is only returned at creation time.share.url is the canonical shareable deep link (when channel transport config is available).Always use invite.share.url when present. Do not manually construct ?start= links if the API already provided one.
Presenting to the guardian: Give the guardian the link with clear copy-paste instructions:
Here's your Telegram invite link:
https://t.me/<botUsername>?start=iv_<token>Share this link with the person you want to invite. When they open it in Telegram and press "Start", they'll automatically be added as a trusted contact and can message the assistant directly.
This link can be used
<maxUses>time(s)<and expires in X hours/days if applicable>.
If the Telegram bot username is not available (integration not set up), tell the guardian they need to set up the Telegram integration first using the Telegram Setup skill.
Use this when the guardian wants to authorize a specific phone number to call the assistant. Voice invites are identity-bound: the invitee must call from the specified phone number AND enter a one-time numeric code.
Important: The response includes a voiceCode field that is only returned at creation time and cannot be retrieved later. Extract and present it clearly.
assistant contacts invites create --source-channel phone --contact-id "<contact_id>" --expected-external-user-id "<phone_E164>" --max-uses 1 --note "<optional note, e.g. the person it is for>" --jsonRequired flags:
--source-channel -- must be phone--contact-id -- the ID of the contact this invite is for. Look up the contact first with assistant contacts list, or create one with assistant contacts create if they are not in the graph yet. The contact's displayName is used to personalize the voice greeting; the guardian label is resolved at runtime.--expected-external-user-id -- the invitee's phone number in E.164 format (e.g., +15551234567)Optional flags:
--max-uses -- how many times the code can be used (default: 1)--expires-in-ms -- expiration time in milliseconds from now (e.g., 86400000 for 24 hours). Defaults to 7 days if omitted.--note -- a human-readable label for the invite (e.g., "For Mom", "Dr. Smith")The create response contains { ok: true, invite: { id, voiceCode, expectedExternalUserId, ... } }.
voiceCode is the numeric code the invitee must enter and is only returned at creation time.token or share.url. Do not try to build or send a deep link for voice invites.Presenting to the guardian: Give the guardian clear instructions to relay to the invitee:
Voice invite created for <phone_number>:
Invite code:
<voiceCode>Share these instructions with the person you are inviting:
- Call the assistant's phone number from <phone_number> (the call must come from this exact number)
- When prompted, enter the code
<voiceCode>- Once verified, they will be added as a trusted contact and can call the assistant directly in the future
This code can be used
<maxUses>time(s)<and expires in X hours/days if applicable>.
There is no "open link" step for voice invites. The invite is redeemed only during a live phone call from the bound number.
If the user provides a phone number without the + country code prefix, ask them to confirm the full E.164 number (e.g., US numbers should be +1XXXXXXXXXX).
Use this when the guardian wants to invite someone to message the assistant via email. Email invites use a 6-digit code - the invitee sends the code to the assistant's email address to redeem access.
assistant contacts invites create --source-channel email --contact-id "<contact_id>" --max-uses 1 --note "<optional note, e.g. the person it is for>" --jsonRequired flags:
--source-channel -- must be email--contact-id -- the ID of the contact this invite is for. Look up the contact first with assistant contacts list, or create one with assistant contacts create if they are not in the graph yet.The response contains { ok: true, invite: { id, token, inviteCode, guardianInstruction, channelHandle, ... } }.
inviteCode is the 6-digit code the invitee must send to redeem the invite. It is only returned at creation time.guardianInstruction is a generated instruction telling the guardian how to share the invite.channelHandle is the assistant's email address (e.g. hello@vellum.me).Presenting to the guardian: Give the guardian the invite code and the assistant's email address:
Email invite created for <contact_name>:
Invite code:
<inviteCode>Tell them to send an email to
<channelHandle>with the code<inviteCode>in the body. Once verified, they will be added as a trusted contact and can email the assistant directly.This code can be used
<maxUses>time(s)<and expires in X hours/days if applicable>.
If the assistant's email address is not available, tell the guardian they need to register one with assistant email register <username>.
Use this when the guardian wants to invite someone to message the assistant on WhatsApp. WhatsApp invites use a 6-digit code - the invitee sends the code to the assistant's WhatsApp number to redeem access.
assistant contacts invites create --source-channel whatsapp --contact-id "<contact_id>" --max-uses 1 --note "<optional note, e.g. the person it is for>" --jsonRequired flags:
--source-channel -- must be whatsapp--contact-id -- the ID of the contact this invite is for. Look up the contact first with assistant contacts list, or create one with assistant contacts create if they are not in the graph yet.The response contains { ok: true, invite: { id, token, inviteCode, guardianInstruction, channelHandle?, ... } }.
inviteCode is the 6-digit code the invitee must send to redeem the invite.guardianInstruction is a generated instruction telling the guardian how to share the invite.channelHandle (optional) is the assistant's WhatsApp display phone number. It is only present when a display number is configured via whatsapp.phoneNumber in workspace config.Presenting to the guardian: If channelHandle is present, give the guardian the invite code and the assistant's WhatsApp number:
WhatsApp invite created for <contact_name>:
Invite code:
<inviteCode>Tell them to send a WhatsApp message to
<channelHandle>with the code<inviteCode>. Once verified, they will be added as a trusted contact and can message the assistant on WhatsApp directly.This code can be used
<maxUses>time(s)<and expires in X hours/days if applicable>.
If channelHandle is absent, present the invite code and tell the guardian to share the code with the invitee, instructing them to send it to the assistant on WhatsApp (without citing a specific number).
If the assistant's WhatsApp integration is not configured at all (Meta WhatsApp Business API credentials missing), tell the guardian they need to set up WhatsApp integration first.
Use this when the guardian wants to invite someone to message the assistant on Slack. Slack invites use a 6-digit code -- the invitee sends the code as a direct message to the assistant's Slack bot to redeem access.
assistant contacts invites create --source-channel slack --contact-id "<contact_id>" --max-uses 1 --note "<optional note, e.g. the person it is for>" --jsonRequired flags:
--source-channel -- must be slack--contact-id -- the ID of the contact this invite is for. Look up the contact first with assistant contacts list, or create one with assistant contacts create if they are not in the graph yet.The response follows the same shape as email and WhatsApp invites (inviteCode, guardianInstruction, channelHandle).
Presenting to the guardian: Give the guardian the invite code and instructions for the invitee to send the code as a DM to the assistant's Slack bot.
Slack invite created for <contact_name>:
Invite code:
<inviteCode>Tell them to send a direct message to the assistant's Slack bot with the code
<inviteCode>. Once verified, they will be added as a trusted contact and can message the assistant on Slack directly.This code can be used
<maxUses>time(s)<and expires in X hours/days if applicable>.
If the Slack bot is not available (Slack credentials not configured), tell the guardian they need to set up Slack integration first.
Use this to show the guardian their active (and optionally all) invite links.
assistant contacts invites list --source-channel telegram --jsonFor voice invites:
assistant contacts invites list --source-channel phone --jsonFor email, WhatsApp, or Slack invites:
assistant contacts invites list --source-channel email --json
assistant contacts invites list --source-channel whatsapp --json
assistant contacts invites list --source-channel slack --jsonOptional query parameters:
--source-channel -- filter by channel (e.g., telegram, phone, email, whatsapp, slack)--status -- filter by status (active, revoked, redeemed, expired)The response contains { ok: true, invites: [...] } where each invite has:
id -- unique invite ID (needed for revoke)sourceChannel -- the channeltokenHash -- hashed token (the raw token is only available at creation time)maxUses -- total allowed usesuseCount -- how many times it has been redeemedexpiresAt -- expiration timestamp (null if no expiration)status -- current status (active, revoked, redeemed, expired)note -- the label set at creationcreatedAt -- when the invite was createdVoice invites also include:
expectedExternalUserId -- the bound phone numbervoiceCodeDigits -- always 6 (the code itself is not retrievable after creation)token and share are not present for voice invitesPresenting results: Format as a readable list. Show the note (or "unnamed" as fallback), status, uses remaining (maxUses - useCount), and expiration. For voice invites, also show the bound phone number. Highlight active invites and note which ones have been fully used or expired.
Use this when the guardian wants to cancel an active invite link or voice invite. Always confirm before revoking.
Ask the user: "I'll revoke the invite [note or ID]. It will no longer be usable. Should I proceed?"
First, list invites to find the invite's id, then revoke:
assistant contacts invites revoke <invite_id> --jsonReplace <invite_id> with the invite's id from the list response. The same revoke command is used for both Telegram and voice invites.
Use google_contacts to list or search the user's Google Contacts by name or email. Returns name, email, phone, and organization. Requires the contacts.readonly OAuth scope - users may need to re-authorize Gmail to grant this additional permission.
Supported channel types: email, slack, whatsapp, phone, telegram, discord, other
Each channel has:
All mutating actions (allow, revoke, block, revoke invite) require explicit user confirmation before execution. This is a safety measure -- modifying who can access the assistant should always be a deliberate choice. Creating an invite (Telegram link, voice invite, email invite, WhatsApp invite, or Slack invite) does not require confirmation since it does not grant access until the invitee redeems it.
{ ok: false, error: "..." }, report the error message to the user. CLI commands also exit with non-zero status on errors.Channel not found -- the channel ID may be invalid; list contacts to find the correct channel ID.Channel already revoked -- the channel has already been revoked.Channel already blocked -- the channel has already been blocked.Cannot revoke a blocked channel -- the channel is blocked; blocking is stronger than revoking. Tell the user the contact is already blocked.sourceChannel is required for create -- when creating an invite, always pass --source-channel.contactId is required for create -- when creating an invite, always pass --contact-id. Look up or create the contact first.expectedExternalUserId is required for voice invites -- voice invites must include the invitee's phone number via --expected-external-user-id.expectedExternalUserId must be in E.164 format -- the phone number must start with + followed by country code and number (e.g., +15551234567).Invite not found or already revoked -- the invite ID may be invalid or the invite is already revoked.--channel-address to find contacts by their email, phone, or handle.contact_id to link the follow-up to a specific contact."Who can message me?" -- List all contacts with assistant contacts list --json, present active channels as a formatted list.
"Add Alice as a contact" / "Remember Bob's my dentist" -- Search first with assistant contacts list --query "<name>" so you update an existing record instead of creating a duplicate. Then assistant contacts create --name "<name>" --notes "<what they told you>", or assistant contacts update <contact_id> if they are already there. Tell the user the form is waiting for them, and report what the command returns rather than what you proposed.
"Rename [name]" / "Update [name]'s notes" -- Find the contact with assistant contacts list --json, then assistant contacts update <contact_id> --name ... --notes ....
"Delete [name]" / "Remove [name] from my contacts" -- Find the contact, then assistant contacts delete <contact_id>. Mention that this drops their channels too. If the user actually means two records are the same person, run assistant contacts merge <surviving_contact_id> <donor_contact_id> instead.
"Add my friend to Telegram" -- Create the contact with assistant contacts create --name "<their name>" and wait for the guardian to confirm the form. Then create an invite with assistant contacts invites create --source-channel telegram --contact-id <contact_id> and present the deep link. You never need their Telegram user ID: redeeming the invite is what binds their account.
"Remove [name]'s access" -- List contacts with assistant contacts list --json to find them, identify the channel to revoke, confirm the revocation, then run assistant contacts channels update-status <channel_id> --status revoked --json.
"Block [name]" -- List contacts with assistant contacts list --json to find them, identify the channel to block, confirm the block, then run assistant contacts channels update-status <channel_id> --status blocked --json.
"Show me blocked contacts" -- List contacts with assistant contacts list --json and filter for channels with status: "blocked".
"Create a Telegram invite link" / "Invite someone on Telegram" -- Look up the contact, or create it with assistant contacts create and wait for the guardian to confirm, then create an invite with assistant contacts invites create --source-channel telegram --contact-id <contact_id>, look up the bot username, build the deep link, and present it with sharing instructions.
"Invite someone by email" / "Send an email invite" -- Look up the contact, or create it with assistant contacts create and wait for the guardian to confirm, then create an invite with assistant contacts invites create --source-channel email --contact-id <contact_id>. Present the 6-digit invite code and the assistant's email address. Tell the guardian to share both with the invitee.
"Invite someone on WhatsApp" -- Look up the contact, or create it with assistant contacts create and wait for the guardian to confirm, then create an invite with assistant contacts invites create --source-channel whatsapp --contact-id <contact_id>. Present the 6-digit invite code. If channelHandle is returned, also present the assistant's WhatsApp number; otherwise, tell the guardian to share the code and instruct the invitee to send it to the assistant on WhatsApp.
"Invite someone on Slack" -- Look up the contact, or create it with assistant contacts create and wait for the guardian to confirm, then create an invite with assistant contacts invites create --source-channel slack --contact-id <contact_id>. Present the 6-digit invite code and tell the guardian to have the invitee DM the code to the assistant's Slack bot.
"Show my invites" / "List active invite links" -- List invites with assistant contacts invites list --source-channel telegram --json, present active invites with uses remaining and expiration info. Use the appropriate --source-channel value for other channels.
"Revoke invite" / "Cancel invite link" -- List invites to identify the target, confirm, then revoke with assistant contacts invites revoke <invite_id> --json.
"Create a voice invite for +15551234567" -- Look up or create the contact first, then create a voice invite with assistant contacts invites create --source-channel phone --contact-id <contact_id> --expected-external-user-id "+15551234567". Present the invite code and instructions: the person must call from that number and enter the code. The caller's name and the guardian's name resolve from the bound contact and the runtime guardian — no name flags needed.
"Let my mom call in" / "Invite someone by phone" -- Ask for the phone number in E.164 format, look up or create the contact first, then create a voice invite with assistant contacts invites create --source-channel phone --contact-id <contact_id>, and present the code + calling instructions.
"Show my voice invites" / "List phone invites" -- List invites with assistant contacts invites list --source-channel phone --json, present active invites with bound phone number and expiration info.
"Revoke voice invite" / "Cancel the phone invite for +15551234567" -- List voice invites, identify the target by phone number or note, confirm, then revoke with assistant contacts invites revoke <invite_id> --json.
© vellum-ai, MIT. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file
SKILL.md and 4 other files in assistant/src/config/bundled-skills/contacts of vellum-ai/vellum-assistant.
Open the folder on GitHubat commit 33cc983
Contacts next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.
| Skill | Stars | Used in | Tokens | Auto-check | Licence | Repo updated |
|---|---|---|---|---|---|---|
| Contacts this skillvellum-ai/vellum-assistant | 1.4k | — | ~8.9k | Automated safety check: Pass | MIT | |
| Configuring Horizoncoollabsio/coolify | 63k | 4 repos | ~898 | Automated safety check: Pass | MIT | |
| K8s Security PoliciesCybereason-Public/owLSM | 280 | 12 repos | ~2k | Automated safety check: Pass | GPL-2.0 | |
| Payloadpayloadcms/payload | 45k | 5 repos | ~6.2k | Automated safety check: Pass | MIT | |
| Convex Setup Authspokvulcan/poker-planning | 114 | 8 repos | ~1.8k | Automated safety check: Pass | MIT | |
| Cognitoitsmostafa/aws-agent-skills | 1.2k | 1 repos | ~2.3k | Automated safety check: Pass | MIT |
coollabsio/coolify
A skill your agent uses whenever the user mentions Horizon by name in a Laravel context.
Cybereason-Public/owLSM
Comprehensive guide for implementing NetworkPolicy, PodSecurityPolicy, RBAC, and Pod Security Standards in Kubernetes.
payloadcms/payload
A skill your agent uses when working with Payload projects (payload.config.ts, collections, fields, hooks, access control, Payload API).
spokvulcan/poker-planning
Sets up Convex auth, identity mapping, and access control. An agent skill from spokvulcan/poker-planning.
itsmostafa/aws-agent-skills
AWS Cognito user authentication and authorization service. An agent skill from itsmostafa/aws-agent-skills.
abpframework/abp
ABP permission system - PermissionDefinitionProvider, [Authorize] attribute, CheckPolicyAsync, IsGrantedAsync, ICurrentUser, IPermissionManager, multi-tenancy side.
vellum-ai/vellum-assistant
Create and configure a GitHub App so the assistant can push commits, open PRs, and comment under its own bot identity.
vellum-ai/vellum-assistant
Connect a Discord bot to the assistant via the Discord Gateway with guided application creation and intent configuration
vellum-ai/vellum-assistant
Create and configure a Sentry internal integration so the assistant can manage issues, alerts, and releases under its own identity
vellum-ai/vellum-assistant
Ingest a large dataset into memory as a skimmed map. An agent skill from vellum-ai/vellum-assistant.
vellum-ai/vellum-assistant
A skill your agent uses when the user wants to build, scaffold, ship, or edit a Vellum plugin that bundles multiple surfaces (hooks, tools, skills, and more) into one installable package.
vellum-ai/vellum-assistant
Connect a Slack app to the Vellum Assistant via Socket Mode.
Categories
Manage contacts, communication channels, access control, and invite links. Contacts is an agent skill from vellum-ai/vellum-assistant.
Contacts fits situations like: tasks that involve Authorization and RBAC.
Run `npx skills add vellum-ai/vellum-assistant --skill contacts -a claude-code`. Or copy the skill folder (assistant/src/config/bundled-skills/contacts in vellum-ai/vellum-assistant) into .claude/skills/contacts in your project. Claude Code loads it when a task matches its description.
Run `npx skills add vellum-ai/vellum-assistant --skill contacts -a codex`. Or copy the skill folder (assistant/src/config/bundled-skills/contacts in vellum-ai/vellum-assistant) into .agents/skills/contacts in your project. Codex loads it when a task matches its description.
Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add vellum-ai/vellum-assistant --skill contacts -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/contacts, .gemini/skills/contacts, .github/skills/contacts and .opencode/skills/contacts in your project.
Going by SKILL.md and its folder, Contacts needs TypeScript for the scripts in its folder, the command-line tools its instructions call (python3) and credentials named INVITE_TOKEN. Our summary lists: Node.js. Compatibility (from SKILL.md): Designed for Vellum personal assistants.
SKILL.md names 1 domain. In commands or code: t.me; the agent is likely to contact it when it follows the instructions. This is read from the text; nothing was executed.
Our automated static check of SKILL.md found no risky patterns, such as piping downloads into a shell, reading credential files or hidden Unicode. It is not a guarantee. Review the folder before installing.
Contacts is published under the MIT licence (the repository's licence). It allows redistribution, so the full SKILL.md is shown on this page.
About 8.9k tokens (SKILL.md is roughly 36k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full.
Skills that share tags, products or a category with Contacts: Configuring Horizon (coollabsio/coolify, 63k stars), K8s Security Policies (Cybereason-Public/owLSM, 280 stars), Payload (payloadcms/payload, 45k stars) and Convex Setup Auth (spokvulcan/poker-planning, 114 stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.
vellum-ai (a GitHub organization) maintains it in vellum-ai/vellum-assistant, which has 1,408 GitHub stars. The repository holds 108 skills in this directory. The repository was last updated on October 9, 2026.
Source: vellum-ai/vellum-assistant on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.