ViksaAIDocs
Start now
Channel Hub

Subject binding & project users

Run user-scoped agents on WhatsApp, Telegram, Teams, and other channels without letting the AI choose who the user is.

The rule (no exceptions)#

QuestionWho decidesExample
May this person use orders_agent?Channel access grants (Hub)WhatsApp grant for *
Whose orders are queried?Platform code (invoker binding)user_id = ACME-1001
Which endpoint runs?AIorders_agent.main.list_my_orders
Optional status filter?AI (only if source: llm)status = open

End-to-end example: Acme orders on WhatsApp#

Acme Corp assigns orders_agent to customers on WhatsApp. Each person has a row in the user directory (acme_user_id, email, phone). When someone messages "list my orders", the platform resolves their phone → row → injects ids → agent queries the DB.

  1. 01Define schema — primary key, fields, and match rules (how WhatsApp/Telegram map to a row).
  2. 02Import users — CSV or SCIM with email, phone, acme_user_id.
  3. 03Channel access — grant orders_agent on WhatsApp (Channel Hub).
  4. 04Agent metadata — mark user_id / user_email as source: invoker.
  5. 05Agent code — read payload["user_id"]; do not parse identity from chat text.
  6. 06Deploy & test — message from a known phone; check audit log for injected fields.

Step 1 — Project user schema#

Configure once per project in Volt → Channel Hub → Customer / User base (strip below the Volt Channels header), or via the project-users admin API. You define your own columns and how each channel matches to a row.

schema/upsert (fields + match_rules)json
{
  "primary_key": "acme_user_id",
  "fields": [
    { "name": "acme_user_id", "type": "string", "required": true, "unique": true },
    { "name": "email", "type": "email", "unique": true },
    { "name": "phone_e164", "type": "phone", "unique": true },
    { "name": "full_name", "type": "string" },
    { "name": "is_admin", "type": "boolean" }
  ],
  "match_rules": [
    { "channel": "whatsapp", "field": "phone_e164", "source": "connector.phone" },
    { "channel": "telegram", "field": "telegram_user_id", "source": "connector.external_user_id" },
    { "channel": "teams", "field": "email", "source": "connector.email" },
    { "channel": "*", "field": "email", "source": "connector.email", "fallback": true }
  ]
}
match_rule sourceMeaning
connector.external_user_idTelegram id, Slack user id, WhatsApp chat id, …
connector.emailProfile or verified email from connector
connector.phonePhone from webhook metadata (normalized E.164)

Step 2 — Bulk user CSV#

Header row must include your primary_key column. Import from the Customers tab in Channel Hub, or via POST /internal/volt/project-users/users/import-csv.

users.csvcsv
acme_user_id,email,phone_e164,full_name,is_admin
ACME-1001,[email protected],+15551234567,Alice Smith,false
ACME-1002,[email protected],+15559876543,Bob Jones,false

Step 3 — Map agent inputs (builder / agent JSON)#

Each input declares a source. Identity fields use invoker plus a bind path into the resolved user row.

orders_agent metadatajson
{
  "agent_alias": "orders_agent",
  "inputs": [
    {
      "name": "user_id",
      "type": "string",
      "source": "invoker",
      "bind": "profile.acme_user_id",
      "description": "Injected by platform — never from chat"
    },
    {
      "name": "user_email",
      "type": "string",
      "source": "invoker",
      "bind": "identifiers.email"
    },
    {
      "name": "status",
      "type": "string",
      "source": "llm",
      "required": false,
      "description": "Optional: open, shipped, cancelled"
    }
  ],
  "agent_endpoints": [
    {
      "endpoint": "orders_agent.main.list_my_orders",
      "description": "List orders for the current customer",
      "status": "enabled",
      "subject_binding": "strict",
      "inputs": [
        { "input_ref": "user_id", "required": true },
        { "input_ref": "user_email", "required": true },
        { "input_ref": "status", "required": false }
      ]
    }
  ]
}
FieldValueNotes
sourceinvoker | llm | constant | contextDefault is llm
bindprofile.acme_user_idDot path into invoker snapshot
subject_bindingstrict | auto | nonestrict rejects LLM attempts to set subject fields
access_leveladminOptional — requires is_admin on user row

Step 4 — Agent Python code#

Functions stay async with a single payload: Dict[str, Any]. Identity is already in payload when your code runs — treat it as trusted platform input.

main.pypy
from typing import Any, Dict

from viksa_ai.runtime import mcp_endpoint


@mcp_endpoint(description="List orders for the authenticated customer")
async def list_my_orders(payload: Dict[str, Any]) -> Dict[str, Any]:
    # Platform injected these — NOT from the user's WhatsApp message
    user_id = payload["user_id"]
    user_email = payload["user_email"]
    status = payload.get("status")  # optional filter from AI

    orders = await orders_db.list(user_id=user_id, status=status)
    return {
        "user_id": user_id,
        "count": len(orders),
        "orders": orders,
    }

Optional snapshot for logging or defense in depth: payload["__viksa_invoker__"] (also in Pulse envelope.invoker).

What the user experiences#

  • Matched user — "list my orders" → agent runs with their acme_user_id.
  • Unmatched user — fixed message from schema (blocked before agent); no AI guessing email.
  • Ambiguous match — two rows share the same phone; blocked until admin fixes data or links identity manually.

Customer directory API#

Admin APIs live on directory-sync-service at gateway prefix https://api.viksaai.com/directory-sync. Runtime identity linking, unmatched users, and audit remain on volt-engine-service at https://api.viksaai.com/volt-engine. Full endpoint tables: Directory sync & project users API.

Customer directory base (directory-sync-service)txt
https://api.viksaai.com/directory-sync/internal/volt/project-users
EndpointUse when
POST /users/listPaginated index — search, data source filters, enabled_only
POST /users/getFull record — profile, data_source, identity links, grants
POST /users/upsertCreate or update a customer (syncs connector primary keys)
POST /users/deleteDelete one customer by customer_user_id
POST /users/delete-bulkStart async purge for up to 500 customers by id list
POST /users/delete-previewCount rows matching search or data-source filters before delete
POST /users/delete-by-filterStart async purge for up to 5000 customers matching filters
POST /users/purge/getPoll purge progress by purge_id
POST /users/purge/listList purge runs; active_only for in-flight jobs
POST /users/data-sources/listGrouped counts by data source (for filters and bulk delete by source)
POST /users/import-csvBulk upsert from CSV (tags rows with csv data_source)
POST /users/export-csvExport customer rows as CSV
POST /users/set-connector-enabledDisable WhatsApp only while Slack stays on
POST /users/attach-channel-grantLink customer + assign channel agents

Each customer stores a data_source object (manual, csv, scim, or directory_sync). The Channel Hub Customers tab shows this in the Data source column.

Precedence: master enabled blocks all connectors; when the account is enabled, each channel has its own connector_enabled flag (defaults to on). Disabling from WhatsApp access or the customer Channels tab updates the same master record and grant.

Use the Unmatched and Audit tabs in Channel Hub, or link connectors from the customer detail drawer. These runtime endpoints stay on volt-engine:

Runtime admin base (volt-engine-service)txt
https://api.viksaai.com/volt-engine/internal/volt/project-users
EndpointUse when
POST /identities/linkManually bind WhatsApp +91… → ACME-1001
POST /identities/unlinkRevoke grant and remove connector link
POST /identities/listLinks for one customer_user_id
POST /identities/list-allPaginated all connector → customer links (customer detail)
POST /unmatched/listSee connector directory users with no project link
POST /audit/listReview injected user_id / email per dispatch

Common mistakes#

MistakeFix
user_id still source: llmChange to source: invoker + bind; redeploy agent
AI asks "what is your email?"Invoker inputs hidden from catalog — use invoker binding
User not found on WhatsAppImport row with phone_e164; check match_rule for whatsapp
Link Matrix per-user rowsUse project_users — Link Matrix is for env/config mappings only