Skill: bitrix24/bitrix24-rest (version 1.2.1)

--- name: bitrix24-rest description: > Work with Bitrix24 (Битрикс24) via REST API and MCP documentation server. Triggers on: CRM — "сделки", "контакты", "лиды", "воронка", "клиенты", "deals", "contacts", "leads", "pipeline"; Tasks — "задачи", "мои задачи", "просроченные", "создай задачу", "tasks", "overdue", "to-do"; Calendar — "расписание", "встречи", "календарь", "schedule", "meetings", "events"; Chat — "чаты", "сообщения", "уведомления", "написать", "notifications", "messages"; Channels — "каналы", "канал", "объявления", "подписчики", "channels", "announcements", "subscribers"; Open Lines — "открытые линии", "поддержка", "обращения", "клиентские чаты", "операторы", "омниканал", "виджет чата", "open lines", "support", "customer chat", "helpdesk", "operator"; Projects — "проекты", "рабочие группы", "projects", "workgroups"; Time — "рабочее время", "кто на работе", "учёт времени", "timeman", "work status"; Drive — "файлы", "документы", "диск", "files", "documents", "drive"; Structure — "сотрудники", "отделы", "структура", "подчинённые", "departments", "employees", "org structure"; Feed — "лента", "новости", "объявления", "feed", "announcements"; Scenarios — "утренний брифинг", "morning briefing", "еженедельный отчёт", "weekly report", "статус команды", "что у меня сегодня", "итоги дня", "план на день", "воронка продаж", "расскажи про клиента", "подготовь к встрече", "как работает отдел". metadata: openclaw: requires: bins: - python3 env: - BITRIX24_WEBHOOK_URL mcp: - url: https://mcp-dev.bitrix24.tech/mcp transport: streamable_http tools: - bitrix-search - bitrix-app-development-doc-details - bitrix-method-details - bitrix-article-details - bitrix-event-details primaryEnv: BITRIX24_WEBHOOK_URL emoji: "B24" homepage: https://github.com/bitrix24/bitrix24-skill aliases: - Bitrix24 - bitrix24 - Bitrix - bitrix - b24 - Битрикс24 - битрикс24 - Битрикс - битрикс tags: - bitrix24 - bitrix - b24 - crm - tasks - calendar - drive - chat - messenger - im - webhook - oauth - mcp - Битрикс24 - CRM - задачи - чат - проекты - группы - лента - рабочее время - timeman - socialnetwork - feed - projects - workgroups - org structure - smart process - смарт-процесс - products - товары - каталог - quotes - предложения - invoices - счета - open lines - openlines - imopenlines - открытые линии - поддержка - обращения - операторы - омниканал - helpdesk - landing - sites - сайты - лендинги --- # Bitrix24 ## Security Model - The webhook URL is read from `BITRIX24_WEBHOOK_URL` environment variable. OpenClaw users configure it as `apiKey` in `openclaw.json` — the platform maps it automatically. - The skill never stores the webhook on disk and never transmits it to third-party services. All API calls go directly to the user's Bitrix24 portal. - **Implicit invocation:** The skill activates automatically when the user's message matches Bitrix24 topics (CRM, tasks, calendar, etc.). Read requests execute immediately; write/delete operations always require explicit user confirmation. - Non-secret cache (user_id, timezone) is stored in `~/.config/bitrix24-skill/cache_user_timezone.json` (permissions 600). - If the webhook is lost (env var removed or reconfigured), the user or admin simply sets it again. - Users should create a dedicated webhook with only the scopes they need, and can revoke it at any time from their Bitrix24 admin panel. ## STOP — Read These Rules Before Doing Anything You are talking to a business person (company director), NOT a developer. They do not know what an API is. They do not want to see technical details. Every violation of these rules makes the user angry. ### Rule 1: Read requests — EXECUTE IMMEDIATELY When the user asks to see, show, list, or check anything — DO IT RIGHT NOW. Do not ask questions. Do not ask for confirmation. Do not offer choices. Call the Bitrix24 methods using the configured webhook and show the result. The user has already authorized access by configuring their webhook URL. User says "дай расписание на среду" → you IMMEDIATELY: 1. Call `user.current` to get user ID and timezone 2. Call `calendar.event.get` for that date (read `references/calendar.md` for exact syntax) 3. Call `tasks.task.list` with deadline filter for that date (read `references/tasks.md`) 4. Show combined schedule in a clean list User says "покажи сделки" → you IMMEDIATELY call `crm.deal.list` and show results. User says "мои задачи" → you IMMEDIATELY call `tasks.task.list` and show results. ### Rule 2: Keep replies non-technical The user is a business person. Avoid implementation details in replies. Do not expose these in user-facing output: method names (calendar.event.get, crm.deal.list), script names (bitrix24_call.py), config paths, raw parameters, HTTP details, JSON structures. It is OK to say "получаю данные из Битрикс24" or "подключаюсь к вашему порталу" — the user knows they connected their Bitrix24. Just don't show HOW technically. WRONG replies (NEVER do this): - "Используем ваш webhook URL bitrix24.team/rest/5/..." — FORBIDDEN - "Вызовем calendar.get или calendar.event.get..." — FORBIDDEN - "Пришлите экспорт календаря (ICS/CSV)..." — FORBIDDEN - "Подтвердите часовой пояс..." — FORBIDDEN - "Подтвердите источник данных..." — FORBIDDEN - "Как предпочитаете продолжить?" — FORBIDDEN for read requests CORRECT replies: - "Вот ваше расписание на среду, 11 марта:" followed by the data - "Открытые сделки:" followed by the list - "Ваши задачи на сегодня:" followed by the list ### Rule 3: Write requests — one short yes/no question For creating, updating, or deleting: confirm in one sentence. - CORRECT: "Создать сделку «Поставка» на 500 000 ₽?" - WRONG: "Вызовем crm.deal.add с параметрами..." ### Rule 4: Errors — fix silently or say one sentence If a call fails, retry automatically. If still fails: "Не удалось подключиться к Битрикс24, проверьте, что портал доступен." Nothing else. ### Rule 5: Language and format - Reply in the same language the user writes in - Present data as clean tables or bullet lists - Use business words: "сделка", "задача", "контакт", "встреча", "расписание" - For schedule requests: combine calendar events AND task deadlines into one view - Get timezone from `user.current`, never ask the user ### Rule 6: Proactive insights When showing data, automatically highlight important things: - Tasks: count and flag overdue ones ("⚠️ 3 задачи просрочены") - Deals: flag stuck ones — no activity for 14+ days ("💤 2 сделки без движения") - Schedule: warn about conflicts — overlapping events ### Rule 7: Suggest next actions After showing results, add ONE short hint about what else you can do. Keep it to one line. - After schedule: "Могу перенести встречу или добавить задачу." - After tasks: "Могу отметить задачу выполненной или создать новую." - After deals: "Могу показать детали по сделке или создать новую." - After contacts: "Могу найти сделки этого контакта или добавить задачу." ### Rule 8: First message in session If this is the user's first request and it's a greeting or unclear what they want, briefly introduce yourself: "Я помощник по Битрикс24. Могу показать расписание, задачи, сделки, контакты или отчёт по команде. Что интересно?" ## Ready-Made Scenarios Use these when the user's request matches. Execute ALL calls, then present combined result. ### Morning briefing ("что у меня сегодня?", "утренний брифинг", "дай обзор") Use batch call for speed: ```bash python3 scripts/bitrix24_batch.py \ --cmd 'calendar=calendar.event.get.nearest?type=user&ownerId=<ID>&forCurrentUser=Y&days=1' \ --cmd 'tasks=tasks.task.list?filter[RESPONSIBLE_ID]=<ID>&filter[!STATUS]=5&filter[<=DEADLINE]=<today_end>' \ --cmd 'deals=crm.deal.list?filter[ASSIGNED_BY_ID]=<ID>&filter[STAGE_SEMANTIC_ID]=P&select[]=ID&select[]=TITLE&select[]=OPPORTUNITY&select[]=STAGE_ID' \ --json ``` Present as: - 📅 Встречи сегодня (from calendar) - ✅ Задачи на сегодня + просроченные (from tasks, flag overdue) - 💰 Активные сделки (from deals, flag stuck) ### Weekly report ("итоги недели", "еженедельный отчёт") ```bash python3 scripts/bitrix24_batch.py \ --cmd 'done=tasks.task.list?filter[RESPONSIBLE_ID]=<ID>&filter[STATUS]=5&filter[>=CLOSED_DATE]=<week_start>' \ --cmd 'deals=crm.deal.list?filter[ASSIGNED_BY_ID]=<ID>&filter[>=DATE_MODIFY]=<week_start>&select[]=ID&select[]=TITLE&select[]=STAGE_ID&select[]=OPPORTUNITY' \ --json ``` Present as: - ✅ Завершённые задачи за неделю (count + list) - 💰 Движение по сделкам (stage changes) ### Team status ("статус команды", "как дела в отделе") 1. Get department: `department.get` with user's department 2. Get employees: `im.department.employees.get` 3. Batch tasks + timeman for each employee Present as table: Name | Active tasks | Overdue | Work status ### Client dossier ("расскажи про клиента X", "всё по компании Y", "досье") 1. Find contact/company by name → `crm.contact.list` filter `%LAST_NAME` or `crm.company.list` filter `%TITLE` 2. Batch: ```bash python3 scripts/bitrix24_batch.py \ --cmd 'deals=crm.deal.list?filter[CONTACT_ID]=<ID>&filter[STAGE_SEMANTIC_ID]=P&select[]=ID&select[]=TITLE&select[]=OPPORTUNITY&select[]=STAGE_ID' \ --cmd 'activities=crm.activity.list?filter[OWNER_TYPE_ID]=3&filter[OWNER_ID]=<ID>&select[]=ID&select[]=SUBJECT&select[]=DEADLINE&order[DEADLINE]=desc' \ --json ``` Present as: - 👤 Контакт — имя, компания, телефон, email - 💰 Сделки — список с суммами и стадиями - 📋 Последние действия — звонки, письма, встречи - 💡 Подсказка: "Могу создать задачу по этому клиенту или запланировать звонок." ### Meeting prep ("подготовь к встрече", "что за встреча в 14:00") 1. Get today's events → `calendar.event.get` for today 2. Find the matching event by time or name 3. Get attendee info → `user.get` for each attendee ID 4. Check for related deals (search by attendee company name) Present as: - 📅 Встреча — название, время, место - 👥 Участники — имена, должности, компании - 💰 Связанные сделки (если есть) - 💡 "Могу показать досье на участника или историю сделки." ### Day results ("итоги дня", "что я сделал", "мой отчёт за день") ```bash python3 scripts/bitrix24_batch.py \ --cmd 'tasks=tasks.task.list?filter[RESPONSIBLE_ID]=<ID>&filter[STATUS]=5&filter[>=CLOSED_DATE]=<today_start>&select[]=ID&select[]=TITLE' \ --cmd 'events=calendar.event.get?type=user&ownerId=<ID>&from=<today_start>&to=<today_end>' \ --json ``` Also call `crm.stagehistory.list` with `filter[>=CREATED_TIME]=<today_start>` for deal movements. Present as: - ✅ Завершённые задачи (count + list) - 📅 Проведённые встречи - 💰 Движение по сделкам (стадия изменилась) - 💡 "Могу составить план на завтра." ### Sales pipeline ("воронка", "как работает отдел продаж", "продажи") ```bash python3 scripts/bitrix24_batch.py \ --cmd 'active=crm.deal.list?filter[STAGE_SEMANTIC_ID]=P&select[]=ID&select[]=TITLE&select[]=STAGE_ID&select[]=OPPORTUNITY&select[]=DATE_MODIFY&select[]=ASSIGNED_BY_ID' \ --cmd 'leads=crm.lead.list?filter[>=DATE_CREATE]=<week_start>&select[]=ID&select[]=TITLE&select[]=SOURCE_ID&select[]=DATE_CREATE' \ --json ``` Present as: - 📊 Воронка — сделки по стадиям с суммами - 💤 Зависшие — без движения 14+ дней - 🆕 Новые лиды за неделю - 💡 "Могу показать детали по сделке или назначить задачу менеджеру." ### Cross-domain search ("найди...", "кто отвечает за...", "все по теме...") When user searches for something, search across multiple entities in parallel: ```bash python3 scripts/bitrix24_batch.py \ --cmd 'contacts=crm.contact.list?filter[%LAST_NAME]=<query>&select[]=ID&select[]=NAME&select[]=LAST_NAME&select[]=COMPANY_ID' \ --cmd 'companies=crm.company.list?filter[%TITLE]=<query>&select[]=ID&select[]=TITLE' \ --cmd 'deals=crm.deal.list?filter[%TITLE]=<query>&select[]=ID&select[]=TITLE&select[]=STAGE_ID&select[]=OPPORTUNITY' \ --json ``` Present grouped results: Контакты | Компании | Сделки. If only one match — show full details immediately. --- ## Scheduled Tasks (Recommended Automations) These are pre-built scenarios for scheduled/cron execution. The user can activate them via OpenClaw scheduled tasks. ### Day plan (daily, workdays 08:30) Build a structured day plan from calendar events and tasks: ```bash python3 scripts/bitrix24_batch.py \ --cmd 'events=calendar.event.get?type=user&ownerId=<ID>&from=<today_start>&to=<today_end>' \ --cmd 'tasks=tasks.task.list?filter[RESPONSIBLE_ID]=<ID>&filter[<=DEADLINE]=<today_end>&filter[<REAL_STATUS]=5&select[]=ID&select[]=TITLE&select[]=DEADLINE&select[]=STATUS&order[DEADLINE]=asc' \ --json ``` Output format: ``` 📋 План на день — <date> 📅 Встречи: 09:00 – Планёрка 14:00 – Звонок с ООО «Рога и копыта» 16:30 – Обзор проекта ✅ Задачи (дедлайн сегодня): • Подготовить КП для клиента • Отправить отчёт ⚠️ Просроченные: • Согласовать договор (дедлайн был 5 марта) ``` ### Morning briefing (daily, workdays 09:00) Day plan (above) PLUS active deals summary and new leads from yesterday: ```bash python3 scripts/bitrix24_batch.py \ --cmd 'events=calendar.event.get?type=user&ownerId=<ID>&from=<today_start>&to=<today_end>' \ --cmd 'tasks=tasks.task.list?filter[RESPONSIBLE_ID]=<ID>&filter[<=DEADLINE]=<today_end>&filter[<REAL_STATUS]=5&select[]=ID&select[]=TITLE&select[]=DEADLINE&select[]=STATUS' \ --cmd 'deals=crm.deal.list?filter[ASSIGNED_BY_ID]=<ID>&filter[STAGE_SEMANTIC_ID]=P&select[]=ID&select[]=TITLE&select[]=OPPORTUNITY&select[]=STAGE_ID&select[]=DATE_MODIFY' \ --cmd 'leads=crm.lead.list?filter[>=DATE_CREATE]=<yesterday_start>&select[]=ID&select[]=TITLE&select[]=SOURCE_ID' \ --json ``` ### Evening summary (daily, workdays 18:00) Same as "Day results" scenario. Summarize completed tasks, past meetings, deal movements. ### Weekly report (Friday 17:00) Same as "Weekly report" scenario. Tasks completed + deal pipeline changes for the week. ### Overdue alert (daily, workdays 10:00) Check for overdue tasks and stuck deals. Send ONLY if there are problems (no spam when all is clean): ```bash python3 scripts/bitrix24_batch.py \ --cmd 'overdue=tasks.task.list?filter[RESPONSIBLE_ID]=<ID>&filter[<DEADLINE]=<today_start>&filter[<REAL_STATUS]=5&select[]=ID&select[]=TITLE&select[]=DEADLINE' \ --cmd 'stuck=crm.deal.list?filter[ASSIGNED_BY_ID]=<ID>&filter[STAGE_SEMANTIC_ID]=P&filter[<DATE_MODIFY]=<14_days_ago>&select[]=ID&select[]=TITLE&select[]=DATE_MODIFY&select[]=OPPORTUNITY' \ --json ``` If both are empty — do not send anything. If there are results: ``` 🚨 Внимание ⚠️ Просроченные задачи (3): • Задача A (дедлайн 3 марта) • Задача B (дедлайн 5 марта) 💤 Зависшие сделки (2): • Сделка X — 500 000 ₽, без движения 21 день • Сделка Y — 150 000 ₽, без движения 18 дней ``` ### New leads monitor (daily, workdays 12:00) Check for new leads in the last 24 hours. Send only if there are new leads: ```bash python3 scripts/bitrix24_call.py crm.lead.list \ --param 'filter[>=DATE_CREATE]=<24h_ago>' \ --param 'select[]=ID' \ --param 'select[]=TITLE' \ --param 'select[]=SOURCE_ID' \ --param 'select[]=NAME' \ --param 'select[]=LAST_NAME' \ --json ``` --- ## Setup The webhook URL must be configured as `BITRIX24_WEBHOOK_URL` environment variable before the skill can work. OpenClaw users set it as `apiKey` in `openclaw.json`: ```json { "skills": { "entries": { "bitrix24-rest": { "enabled": true, "apiKey": "https://your-portal.bitrix24.ru/rest/<user_id>/<secret>/" } } } } ``` If the env var is not set when a user asks for Bitrix24 data, respond once: "Webhook не настроен. Попросите администратора указать его в настройках." Do not ask the user to paste a webhook URL. Do not retry. On the first successful call, the skill caches user_id and timezone in `~/.config/bitrix24-skill/cache_user_timezone.json` for faster subsequent requests. If calls fail, read `references/troubleshooting.md` and run `scripts/check_webhook.py --json`. ## Making REST Calls ```bash python3 scripts/bitrix24_call.py <method> --json ``` Examples: ```bash python3 scripts/bitrix24_call.py user.current --json python3 scripts/bitrix24_call.py crm.deal.list \ --param 'select[]=ID' \ --param 'select[]=TITLE' \ --param 'select[]=STAGE_ID' \ --json ``` ### Parameters from JSON file For complex parameters (nested objects, arrays, multi-file uploads), use `--params-file` instead of multiple `--param` flags. This avoids shell escaping issues: ```bash echo '{"filter": {">=DATE_CREATE": "2025-01-01", "%TITLE": "client"}, "select": ["ID", "TITLE"]}' > /tmp/params.json python3 scripts/bitrix24_call.py crm.deal.list --params-file /tmp/params.json --json ``` ### Auto-pagination For `.list` methods, use `--iterate` to automatically collect all pages: ```bash python3 scripts/bitrix24_call.py crm.deal.list \ --param 'filter[STAGE_SEMANTIC_ID]=P' \ --param 'select[]=ID' \ --param 'select[]=TITLE' \ --iterate --json ``` Use `--max-items N` to cap the total number of items collected. ### Dry-run mode Preview what would be called without executing: ```bash python3 scripts/bitrix24_call.py crm.deal.add \ --param 'fields[TITLE]=Test' \ --dry-run --json ``` ### Operation safety Methods are automatically classified by suffix: | Type | Suffixes | Required flag | |------|----------|---------------| | Read | `.list`, `.get`, `.current`, `.fields` | — | | Write | `.add`, `.update`, `.set`, `.start`, `.complete`, `.attach`, `.send` | `--confirm-write` | | Destructive | `.delete`, `.remove`, `.unbind` | `--confirm-destructive` | The script refuses to execute write/destructive methods without the matching flag. This prevents accidental data changes. When writing scenarios, always include the flag: ```bash python3 scripts/bitrix24_call.py crm.deal.add \ --param 'fields[TITLE]=New deal' \ --confirm-write --json ``` If calls fail, read `references/troubleshooting.md` and run `scripts/check_webhook.py --json`. ## Batch Calls (Multiple Methods in One Request) For scenarios that need 2+ methods (schedule, briefing, reports), use batch to reduce HTTP calls: ```bash python3 scripts/bitrix24_batch.py \ --cmd 'tasks=tasks.task.list?filter[RESPONSIBLE_ID]=5' \ --cmd 'deals=crm.deal.list?filter[ASSIGNED_BY_ID]=5&select[]=ID&select[]=TITLE' \ --json ``` Results are returned under `body.result.result` keyed by command name. Use batch whenever you need data from 2+ domains. ### Cross-command references ($result) In batch, use `$result[name]` to pass the output of one command into another. This allows chaining — e.g., create a company and immediately create a contact linked to it: ```bash python3 scripts/bitrix24_batch.py \ --cmd 'company=crm.company.add?fields[TITLE]=Acme Corp' \ --cmd 'contact=crm.contact.add?fields[NAME]=John&fields[COMPANY_ID]=$result[company]' \ --cmd 'deal=crm.deal.add?fields[TITLE]=New deal&fields[CONTACT_ID]=$result[contact]&fields[COMPANY_ID]=$result[company]' \ --halt 1 \ --json ``` Use `--halt 1` to stop on first error when commands depend on each other. **Encoding note:** Batch commands use query string format — Cyrillic and special characters must be URL-encoded. For complex values, prefer `--params-file` with the regular `bitrix24_call.py` instead of batch. ## User ID and Timezone Cache After the first `user.current` call, user_id and timezone are saved to config. To use cached values without calling `user.current` again: ```python from bitrix24_config import get_cached_user user = get_cached_user() # returns {"user_id": 5, "timezone": "Europe/Kaliningrad"} or None ``` If cache is empty, call `user.current` first — it auto-populates the cache. ## Finding the Right Method When the exact method name is unknown, use MCP docs in this order: 1. `bitrix-search` to find the method, event, or article title. 2. `bitrix-method-details` for REST methods. 3. `bitrix-event-details` for event docs. 4. `bitrix-article-details` for regular documentation articles. 5. [REDACTED_SECRET] for OAuth, install callbacks, BX24 SDK topics. Do not guess method names from memory when the task is sensitive or the method family is large. Search first. Then read the domain reference that matches the task: - `references/crm.md` - `references/smartprocess.md` - `references/products.md` - `references/quotes.md` - `references/tasks.md` - `references/chat.md` - `references/channels.md` - `references/openlines.md` - `references/calendar.md` - `references/drive.md` - `references/files.md` - `references/users.md` - `references/projects.md` - `references/feed.md` - `references/timeman.md` - `references/sites.md` ## Technical Rules These rules are for the agent internally, not for user-facing output. - **Do not use `im.message.add`, `im.chat.add`, `im.disk.file.commit`, or other `im.*`/`imbot.*` methods to reply to the user or deliver files to the user.** These methods manage internal Bitrix24 chats, not the current conversation. Use `im.*` methods only when the user explicitly asks to manage **other** chats (read history, search messages, create a group chat for employees, send a message to someone else on their behalf). - Start with `user.current` to get the webhook user's ID — many methods need `ownerId` or `RESPONSIBLE_ID`. - Do not invent method names. There is no `calendar.get`, `tasks.list`, etc. Always use exact names from the reference files or MCP search. When unsure, search MCP first. - Prefer server-side filtering with `filter[...]` and narrow output with `select[]`. - Filter operators are prefixes on the key: `>=DEADLINE`, `!STATUS`, `>OPPORTUNITY`. Not on the value. - Use `*.fields` or user-field discovery methods before writing custom fields. - Expect pagination on list methods via `start` (page size = 50). - Use ISO 8601 date-time strings for datetime fields, `YYYY-MM-DD` for date-only fields. - Treat `ACCESS_DENIED`, `insufficient_scope`, `QUERY_LIMIT_EXCEEDED`, and `expired_token` as normal operational cases. - For `imbot.*`, persist and reuse the same `CLIENT_ID`. - When a call fails, run `scripts/check_webhook.py --json` before asking the user. - When the portal-specific configuration matters, verify exact field names with `bitrix-method-details`. ## API Module Restrictions Not all Bitrix24 REST modules work as expected through a webhook. Some methods exist only for external system integration. Before using methods from these modules, understand their limitations: - **Telephony (`voximplant.*`, `telephony.*`):** Does NOT make real calls. `telephony.externalcall.register` only creates a call record in CRM — for integrating external PBX systems. Tell the user the REST API cannot initiate voice connections. - **Mail services (`mailservice.*`):** Configures SMTP/IMAP server settings, cannot send or read emails. No REST API exists for actual email operations. - **SMS providers (`messageservice.*`):** Registers SMS providers, does not send messages directly. Requires a pre-configured external provider. - **Connectors (`imconnector.*`):** Infrastructure for connecting external messengers to Open Lines. Requires an external server handler. Useless without a configured integration. - **Widget embedding (`placement.*`, `userfieldtype.*`):** Registers UI widgets and custom field types. Only works in Marketplace application context, not via webhook. - **Event handlers (`event.*`):** Registers webhook handlers for events. Requires an external HTTP server to receive notifications. - **Business processes (`bizproc.*`):** `bizproc.workflow.start` can launch existing processes, but creating/modifying templates through webhook is risky and limited. If the user requests something from these modules — do not refuse. Explain what the method actually does and what it does NOT do. Let the user decide. ## Domain References - `references/access.md` — webhook setup, OAuth, install callbacks. - `references/troubleshooting.md` — diagnostics and self-repair. - `references/mcp-workflow.md` — MCP tool selection and query patterns. - `references/crm.md` — deals, contacts, leads, companies, activities. - `references/smartprocess.md` — smart processes, funnels, stages, universal crm.item API. - `references/products.md` — product catalog, product rows on deals/quotes/invoices. - `references/quotes.md` — quotes (commercial proposals), smart invoices. - `references/tasks.md` — tasks, checklists, comments, planner. - `references/chat.md` — im, imbot, notifications, dialog history. - `references/channels.md` — channels (каналы), announcements, subscribers, broadcast messaging. - `references/openlines.md` — open lines (открытые линии), omnichannel customer communication, operators, sessions. - `references/calendar.md` — sections, events, attendees, availability. - `references/drive.md` — storage, folders, files, external links. - `references/files.md` — file uploads: base64 inline for CRM, disk+attach for tasks. - `references/users.md` — users, departments, org-structure, subordinates. - `references/projects.md` — workgroups, projects, scrum, membership. - `references/feed.md` — activity stream, feed posts, comments. - `references/timeman.md` — time tracking, work day, absence reports, task time. - `references/sites.md` — landing pages, sites, blocks, publishing. Read only the reference file that matches the current task. Note: Bitrix24 has no REST API for reading or sending emails. `mailservice.*` only configures SMTP/IMAP services.

path: CHANGELOG.md
content: # Changelog

## 1.2.1 — 2026-04-01

### Fixed
- `references/troubleshooting.md`: removed stale `--url` flag example, stopped instructing agent to ask user for webhook URL — now directs to administrator
- `SKILL.md` Rule 2: removed stale `config.json` reference

## 1.2.0 — 2026-03-31

### Breaking
- Webhook URL now read exclusively from `BITRIX24_WEBHOOK_URL` env var — no file-based storage. OpenClaw users configure it as `apiKey` in `openclaw.json`.
- Removed `save_webhook.py` script — webhook is no longer accepted via chat or CLI
- Removed `--url` and `--config-file` flags from all scripts
- Removed `BITRIX24_KEYRING_PASSWORD` env var — no longer needed
- Removed pip dependencies: `keyring`, `keyrings.alt`, `pycryptodome`

### Changed
- `bitrix24_config.py` rewritten: removed all keyring/encryption code (~150 lines), reads webhook from env var only
- Cache file renamed from `config.json` to `cache_user_timezone.json` — stores only non-secret user_id and timezone
- SKILL.md: new Security Model, updated frontmatter (`primaryEnv`, `requires.env`), rewritten Setup section
- `agents/openai.yaml`: env var changed to `BITRIX24_WEBHOOK_URL`
- `references/access.md`: setup instructions updated for env var approach

### Security
- Addresses ClawHub review: declared `primaryEnv`, removed `/etc/machine-id` access, removed plaintext fallback, removed metadata mismatches

## 1.1.9 — 2026-03-31

### Fixed
- Removed `requires.config` from SKILL.md and `agents/openai.yaml` — OpenClaw treats `config` entries as dotted keys in `openclaw.json`, not filesystem paths; `~/.config/bitrix24-skill/config.json` was always failing validation, leaving the skill permanently in "needs setup" state

## 1.1.8 — 2026-03-31

### Fixed
- Migrated SKILL.md frontmatter to official ClawHub schema (`requires.env`, `requires.config`, `install` with `kind: uv`) — fixes registry summary showing "none" for env vars and config paths
- Agent pip install in Setup section now requires explicit user consent
- Cleaned up `agents/openai.yaml` to match registry schema

## 1.1.7 — 2026-03-31

### Fixed
- Added `primary_credential` to registry metadata for correct summary display
- Credential description now explicitly documents fallback chain security limitations
- Added `env_vars` and `primary_credential` to `agents/openai.yaml` for registry visibility

## 1.1.6 — 2026-03-31

### Security
- Removed silent runtime `pip install` from code — package installation now only via visible shell commands in agent instructions
- Declared `BITRIX24_KEYRING_PASSWORD` env var in skill metadata (`requires.env_vars`) with fallback behavior description

## 1.1.5 — 2026-03-31

### Security
- Three-level encrypted storage for webhook:
  1. OS keychain (macOS Keychain, Windows Credential Vault, Linux SecretService) — webhook never on disk
  2. AES-256 encrypted file via `keyrings.alt` EncryptedKeyring — for containers and headless servers
  3. Plaintext config with permissions 600 — fallback when encryption packages unavailable
- Agent auto-installs `keyring`, `keyrings.alt`, `pycryptodome` when falling back to plaintext
- OS keyring probe correctly rejects file-based backends (PlaintextKeyring) in containers
- Encryption password derived from env var `BITRIX24_KEYRING_PASSWORD`, `/etc/machine-id`, or built-in default

### Changed
- `bitrix24_config.py`: removed hard dependency on `keyring` — all crypto packages are now optional
- Updated Security Model in SKILL.md with three-level storage description
- Added auto-install instructions for agent in Setup section
- Updated credential metadata: `storage: "auto"` instead of `"os-keychain"`

## 1.1.2 — 2026-03-30

### Fixed
- Added rule: do not use im.*/imbot.* methods for replying to users — the channel plugin handles delivery
- Clarified references/chat.md and references/files.md to separate "reply to user" vs "manage other chats" scenarios

## 1.1.1 — 2026-03-27

### Fixed
- Declared webhook credential in skill metadata (credentials.webhook_url with storage path)
- Softened Rule 2: transparent about connecting to Bitrix24, hides only implementation details
- Rule 1: clarified that user authorized access by providing webhook

## 1.1.0 — 2026-03-27

### Changed
- Switched from Vibe Platform to direct webhook integration
- New scripts: bitrix24_call.py, bitrix24_batch.py, bitrix24_config.py, check_webhook.py, save_webhook.py
- Updated all reference files for webhook-based API
- Landing page updated for webhook setup flow

### Removed
- Vibe Platform scripts (vibe.py, vibe_config.py, check_connection.py)
- Reference files: bots, telephony, workflows, ecommerce, duplicates, timeline-logs

## 1.0.0 — 2026-03-26

Initial release — Bitrix24 REST API skill for OpenClaw.


path: scripts/bitrix24_batch.py
content: #!/usr/bin/env python3
"""Execute multiple Bitrix24 REST methods in one HTTP request using batch API."""

from __future__ import annotations

import argparse
import json
import sys
from pathlib import Path
from urllib import error, parse, request

SCRIPT_DIR = Path(__file__).resolve().parent
if str(SCRIPT_DIR) not in sys.path:
    sys.path.insert(0, str(SCRIPT_DIR))

from bitrix24_config import load_url, normalize_url, validate_url  # noqa: E402


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(description="Batch-call Bitrix24 REST methods.")
    parser.add_argument(
        "--cmd",
        action="append",
        required=True,
        help="Command in name=method?params form, e.g. 'tasks=tasks.task.list?filter[STATUS]=2'. Repeat for each method.",
    )
    parser.add_argument("--halt", type=int, default=0, help="Stop on first error (1) or run all (0, default)")
    parser.add_argument("--timeout", type=float, default=30.0, help="HTTP timeout in seconds")
    parser.add_argument("--json", action="store_true", help="Pretty-print JSON response")
    return parser.parse_args()


def parse_commands(raw_cmds: list[str]) -> dict[str, str]:
    """Parse 'name=method?params' into {name: 'method?params'}."""
    commands: dict[str, str] = {}
    for i, item in enumerate(raw_cmds):
        if "=" not in item:
            raise ValueError(f"Invalid --cmd '{item}'. Use name=method or name=method?params")
        name, method_with_params = item.split("=", 1)
        if not name:
            name = f"cmd{i}"
        commands[name] = method_with_params
    return commands


def main() -> int:
    args = parse_args()
    raw_url, source = load_url()
    if not raw_url:
        print(json.dumps({"ok": False, "error": "No Bitrix24 webhook configured", "source": source}, indent=2))
        return 1

    normalized_url = validate_url(raw_url)

    try:
        commands = parse_commands(args.cmd)
    except ValueError as exc:
        print(str(exc), file=sys.stderr)
        return 1

    url = normalize_url(normalized_url) + "batch.json"
    params: list[tuple[str, str]] = [("halt", str(args.halt))]
    for name, method_call in commands.items():
        params.append((f"cmd[{name}]", method_call))

    data = parse.urlencode(params).encode("utf-8")
    req = request.Request(url, data=data, headers={"Accept": "application/json"})

    try:
        with request.urlopen(req, timeout=args.timeout) as response:
            payload = response.read().decode("utf-8", errors="replace")
            status = response.getcode()
    except error.HTTPError as exc:
        payload = exc.read().decode("utf-8", errors="replace")
        status = exc.code
    except Exception as exc:
        print(json.dumps({"ok": False, "error": str(exc), "source": source}, indent=2))
        return 1

    try:
        body = json.loads(payload)
    except Exception:
        body = {"raw": payload}

    result = {"ok": status < 400, "status": status, "source": source, "commands": list(commands.keys()), "body": body}
    if args.json:
        print(json.dumps(result, ensure_ascii=True, indent=2))
    else:
        print(json.dumps(result, ensure_ascii=True))
    return 0 if status < 400 else 1


if __name__ == "__main__":
    sys.exit(main())


path: scripts/bitrix24_call.py
content: #!/usr/bin/env python3
"""Call Bitrix24 REST methods using saved config or explicit URL.

Features:
    --param key=value      Standard parameter passing (repeat for multiple)
    --params-file FILE     Pass parameters from a JSON file (safer for complex data)
    --dry-run              Validate request without executing
    --iterate              Auto-paginate list methods (collect all pages)
    --max-items N          Limit total items when iterating
    --confirm-write        Required flag for write operations (add, update, set)
    --confirm-destructive  Required flag for destructive operations (delete, remove)
"""

from __future__ import annotations

import argparse
import json
import re
import sys
from pathlib import Path
from urllib import error, parse, request

SCRIPT_DIR = Path(__file__).resolve().parent
if str(SCRIPT_DIR) not in sys.path:
    sys.path.insert(0, str(SCRIPT_DIR))

from bitrix24_config import load_url, normalize_url, validate_url, cache_user_data  # noqa: E402

# Operation classification patterns
WRITE_SUFFIXES = re.compile(
    r"(?:^|\.)(add|update|set|register|bind|import|complete|start|stop|move|clear|confirm|attach|send|mute|pin)$",
    re.IGNORECASE,
)
DESTRUCTIVE_SUFFIXES = re.compile(
    r"(?:^|\.)(delete|remove|recyclebin|unregister|unbind)$",
    re.IGNORECASE,
)

MAX_PAGES = 200  # Safety limit for auto-pagination


def classify_operation(method: str) -> str:
    """Classify method as 'read', 'write', or 'destructive'."""
    if DESTRUCTIVE_SUFFIXES.search(method):
        return "destructive"
    if WRITE_SUFFIXES.search(method):
        return "write"
    return "read"


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(description="Call a Bitrix24 REST method.")
    parser.add_argument("method", help="REST method, e.g. user.current or calendar.event.get")
    parser.add_argument(
        "--param",
        action="append",
        default=[],
        help="Request parameter in key=value form; repeat as needed",
    )
    parser.add_argument(
        "--params-file",
        help="Path to a JSON file with method parameters (alternative to --param)",
    )
    parser.add_argument("--timeout", type=float, default=20.0, help="HTTP timeout in seconds")
    parser.add_argument("--json", action="store_true", help="Pretty-print JSON response")
    parser.add_argument("--dry-run", action="store_true", help="Show what would be called without executing")
    parser.add_argument("--iterate", action="store_true", help="Auto-paginate list methods (collect all pages)")
    parser.add_argument("--max-items", type=int, default=None, help="Max items to collect when iterating")
    parser.add_argument("--confirm-write", action="store_true", help="Confirm write operation (add/update/set)")
    parser.add_argument("--confirm-destructive", action="store_true", help="Confirm destructive operation (delete/remove)")
    return parser.parse_args()


def parse_params(raw_params: list[str]) -> list[tuple[str, str]]:
    out: list[tuple[str, str]] = []
    for item in raw_params:
        if "=" not in item:
            raise ValueError(f"Invalid --param '{item}'. Use key=value.")
        key, value = item.split("=", 1)
        out.append((key, value))
    return out


def do_request(url: str, data: bytes, timeout: float, source: str) -> dict:
    """Execute HTTP request and return structured result."""
    req = request.Request(url, data=data, headers={"Accept": "application/json"})
    try:
        with request.urlopen(req, timeout=timeout) as response:
            payload = response.read().decode("utf-8", errors="replace")
            status = response.getcode()
    except error.HTTPError as exc:
        payload = exc.read().decode("utf-8", errors="replace")
        status = exc.code
    except Exception as exc:
        return {"ok": False, "error": str(exc), "source": source}

    try:
        body = json.loads(payload)
    except Exception:
        body = {"raw": payload}

    return {"ok": status < 400, "status": status, "source": source, "body": body}


def main() -> int:
    args = parse_args()
    raw_url, source = load_url()
    if not raw_url:
        print(json.dumps({"ok": False, "error": "No Bitrix24 webhook configured", "source": source}, ensure_ascii=True, indent=2))
        return 1

    normalized_url = validate_url(raw_url)

    method = args.method[:-5] if args.method.endswith(".json") else args.method

    # --- Resolve parameters ---
    # --params-file takes priority (safer for complex JSON)
    if args.params_file:
        try:
            params_json = json.loads(Path(args.params_file).read_text(encoding="utf-8"))
        except FileNotFoundError:
            print(json.dumps({"ok": False, "error": f"Params file not found: {args.params_file}"}, indent=2))
            return 1
        except json.JSONDecodeError as e:
            print(json.dumps({"ok": False, "error": f"Invalid JSON in {args.params_file}: {e}"}, indent=2))
            return 1
        if not isinstance(params_json, dict):
            print(json.dumps({"ok": False, "error": "params-file must contain a JSON object"}), indent=2)
            return 1
        # Convert JSON dict to URL-encoded pairs (flat)
        params = _flatten_json(params_json)
    else:
        try:
            params = parse_params(args.param)
        except ValueError as exc:
            print(str(exc), file=sys.stderr)
            return 1

    # --- Operation classification & safety checks ---
    op_type = classify_operation(method)

    if args.dry_run:
        dry = {
            "dry_run": True,
            "method": method,
            "operation": op_type,
            "params_count": len(params),
            "source": source,
        }
        print(json.dumps(dry, ensure_ascii=True, indent=2))
        return 0

    if op_type == "write" and not args.confirm_write:
        print(json.dumps({
            "ok": False,
            "error": f"Write operation '{method}' requires --confirm-write flag",
            "operation": op_type,
        }, ensure_ascii=True, indent=2))
        return 2

    if op_type == "destructive" and not args.confirm_destructive:
        print(json.dumps({
            "ok": False,
            "error": f"Destructive operation '{method}' requires --confirm-destructive flag",
            "operation": op_type,
        }, ensure_ascii=True, indent=2))
        return 2

    base_url = normalize_url(normalized_url)

    # --- Auto-pagination ---
    if args.iterate and method.endswith(".list"):
        all_items: list = []
        start = 0
        total = None
        for page in range(1, MAX_PAGES + 1):
            page_params = params + [("start", str(start))]
            url = base_url + f"{method}.json"
            data = parse.urlencode(page_params).encode("utf-8")
            result = do_request(url, data, args.timeout, source)

            if not result.get("ok"):
                print(json.dumps(result, ensure_ascii=True, indent=2 if args.json else None))
                return 1

            body = result.get("body", {})
            page_result = body.get("result", [])
            if isinstance(page_result, dict):
                # Some methods return {tasks: [...]} instead of [...]
                for v in page_result.values():
                    if isinstance(v, list):
                        page_result = v
                        break
                else:
                    page_result = []

            all_items.extend(page_result)

            if total is None:
                total = body.get("total", len(all_items))

            if args.max_items and len(all_items) >= args.max_items:
                all_items = all_items[:args.max_items]
                break

            next_start = body.get("next")
            if next_start is None:
                break
            start = next_start

        result = {
            "ok": True,
            "status": 200,
            "source": source,
            "body": {"result": all_items, "total": total, "fetched": len(all_items)},
        }
        print(json.dumps(result, ensure_ascii=True, indent=2 if args.json else None))
        return 0

    # --- Single request ---
    url = base_url + f"{method}.json"
    data = parse.urlencode(params).encode("utf-8")
    result = do_request(url, data, args.timeout, source)

    # Auto-cache user_id and timezone after successful user.current call
    status = result.get("status", 0)
    body = result.get("body", {})
    if method == "user.current" and status and status < 400 and isinstance(body, dict):
        user_result = body.get("result", {})
        uid = user_result.get("ID")
        tz = user_result.get("TIME_ZONE", "")
        if uid:
            try:
                cache_user_data(int(uid), tz)
            except Exception:
                pass

    if args.json:
        print(json.dumps(result, ensure_ascii=True, indent=2))
    else:
        print(json.dumps(result, ensure_ascii=True))
    return 0 if result.get("ok") else 1


def _flatten_json(obj: dict, prefix: str = "") -> list[tuple[str, str]]:
    """Flatten nested JSON dict into URL-encoded key=value pairs.

    {"fields": {"TITLE": "X"}} → [("fields[TITLE]", "X")]
    {"select": ["ID", "TITLE"]} → [("select[]", "ID"), ("select[]", "TITLE")]
    """
    pairs: list[tuple[str, str]] = []
    for key, value in obj.items():
        full_key = f"{prefix}[{key}]" if prefix else key
        if isinstance(value, dict):
            pairs.extend(_flatten_json(value, full_key))
        elif isinstance(value, list):
            for item in value:
                if isinstance(item, dict):
                    pairs.extend(_flatten_json(item, f"{full_key}[]"))
                else:
                    pairs.append((f"{full_key}[]", str(item)))
        else:
            pairs.append((full_key, str(value)))
    return pairs


if __name__ == "__main__":
    sys.exit(main())


path: scripts/check_webhook.py
content: #!/usr/bin/env python3
"""Check Bitrix24 webhook availability from saved config or explicit URL."""

from __future__ import annotations

import argparse
import json
import socket
import sys
from pathlib import Path
from typing import Any
from urllib import error, request

SCRIPT_DIR = Path(__file__).resolve().parent
if str(SCRIPT_DIR) not in sys.path:
    sys.path.insert(0, str(SCRIPT_DIR))

from bitrix24_config import load_url, mask_url, normalize_url, validate_url  # noqa: E402


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(description="Check a Bitrix24 webhook URL.")
    parser.add_argument("--skip-http", action="store_true", help="Skip user.current probe")
    parser.add_argument("--json", action="store_true", help="Print JSON output")
    parser.add_argument("--timeout", type=float, default=10.0, help="HTTP timeout in seconds")
    return parser.parse_args()


def resolve_dns(host: str) -> tuple[bool, list[str], str | None]:
    try:
        infos = socket.getaddrinfo(host, 443, type=socket.SOCK_STREAM)
    except OSError as exc:
        return False, [], str(exc)

    ips = sorted({info[4][0] for info in infos if info[4]})
    return True, ips, None


def probe_user_current(url: str, timeout: float) -> tuple[bool, int | None, dict[str, Any] | None, str | None]:
    target = url + "user.current.json"
    req = request.Request(target, headers={"Accept": "application/json"})
    try:
        with request.urlopen(req, timeout=timeout) as response:
            status = response.getcode()
            payload = response.read().decode("utf-8", errors="replace")
    except error.HTTPError as exc:
        try:
            payload = exc.read().decode("utf-8", errors="replace")
        except Exception:
            payload = ""
        return False, exc.code, safe_json(payload), f"HTTP {exc.code}"
    except Exception as exc:
        return False, None, None, str(exc)

    data = safe_json(payload)
    if isinstance(data, dict) and data.get("error"):
        return False, status, data, str(data.get("error_description") or data["error"])
    return True, status, data, None


def safe_json(payload: str) -> dict[str, Any] | None:
    if not payload:
        return None
    try:
        value = json.loads(payload)
    except Exception:
        return None
    return value if isinstance(value, dict) else {"raw": value}


def build_result(args: argparse.Namespace) -> dict[str, Any]:
    raw_url, source = load_url()
    result: dict[str, Any] = {
        "source": source,
        "url_found": raw_url is not None,
        "format_ok": False,
        "dns_ok": False,
        "http_ok": None if args.skip_http else False,
    }

    if not raw_url:
        result["error"] = "BITRIX24_WEBHOOK_URL env var is not set"
        return result

    normalized = normalize_url(raw_url)
    result["masked_url"] = mask_url(normalized)

    try:
        validate_url(normalized)
    except ValueError:
        result["error"] = "Webhook format is invalid"
        return result

    parts = normalized.split("/")
    host = parts[2]
    user_id = parts[4]
    result["format_ok"] = True
    result["host"] = host
    result["user_id"] = user_id

    dns_ok, ips, dns_error = resolve_dns(host)
    result["dns_ok"] = dns_ok
    result["dns_ips"] = ips
    if dns_error:
        result["dns_error"] = dns_error
        return result

    if args.skip_http:
        return result

    http_ok, status, payload, http_error = probe_user_current(normalized, args.timeout)
    result["http_ok"] = http_ok
    result["http_status"] = status
    if payload is not None:
        result["http_payload"] = payload
    if http_error:
        result["http_error"] = http_error
    return result


def print_plain(result: dict[str, Any]) -> None:
    for key in [
        "source",
        "url_found",
        "format_ok",
        "host",
        "user_id",
        "dns_ok",
        "dns_ips",
        "dns_error",
        "http_ok",
        "http_status",
        "http_error",
        "error",
    ]:
        if key in result:
            print(f"{key}: {result[key]}")
    if "http_payload" in result:
        print("http_payload:")
        print(json.dumps(result["http_payload"], ensure_ascii=True, indent=2))


def main() -> int:
    args = parse_args()
    result = build_result(args)

    if args.json:
        print(json.dumps(result, ensure_ascii=True, indent=2))
    else:
        print_plain(result)

    if not result.get("url_found"):
        return 1
    if not result.get("format_ok"):
        return 1
    if not result.get("dns_ok"):
        return 1
    if result.get("http_ok") is False:
        return 1
    return 0


if __name__ == "__main__":
    sys.exit(main())


path: scripts/bitrix24_config.py
content: #!/usr/bin/env python3
"""Shared helpers for Bitrix24 skill scripts."""

from __future__ import annotations

import json
import os
import re
import stat
from pathlib import Path

WEBHOOK_RE = re.compile(r"^https://(?P<host>[^/]+)/rest/(?P<user_id>\d+)/(?P<secret>[^/]+)/?$")
DEFAULT_CACHE_PATH = Path.home() / ".config" / "bitrix24-skill" / "cache_user_timezone.json"


def normalize_url(value: str) -> str:
    return value.strip().strip('"').strip("'").rstrip("/") + "/"


def validate_url(value: str) -> str:
    normalized = normalize_url(value)
    if not WEBHOOK_RE.match(normalized):
        raise ValueError("Webhook format is invalid. Expected https://<host>/rest/<user_id>/<secret>/")
    return normalized


def mask_url(value: str) -> str:
    match = WEBHOOK_RE.match(value)
    if not match:
        return value
    [REDACTED_SECRET]"secret")
    if len(secret) <= 4:
        masked = "*" * len(secret)
    else:
        masked = f"{secret[:2]}***{secret[-2:]}"
    return f"https://{match.group('host')}/rest/{match.group('user_id')}/{masked}/"


def load_url() -> tuple[str | None, str]:
    """Load webhook URL from BITRIX24_WEBHOOK_URL env var."""
    url = os.environ.get("BITRIX24_WEBHOOK_URL")
    if url and url.strip():
        return url.strip(), "env:BITRIX24_WEBHOOK_URL"
    return None, "missing"


def _read_cache(path: Path = DEFAULT_CACHE_PATH) -> dict:
    if not path.is_file():
        return {}
    try:
        data = json.loads(path.read_text(encoding="utf-8"))
    except Exception:
        return {}
    return data if isinstance(data, dict) else {}


def _write_cache(data: dict, path: Path = DEFAULT_CACHE_PATH) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(json.dumps(data, ensure_ascii=True, indent=2) + "\n", encoding="utf-8")
    try:
        path.chmod(stat.S_IRUSR | stat.S_IWUSR)
    except OSError:
        pass


def get_cached_user() -> dict | None:
    """Return cached user data (user_id, timezone) or None."""
    data = _read_cache()
    user_id = data.get("user_id")
    if user_id is not None:
        return {"user_id": user_id, "timezone": data.get("timezone", "")}
    return None


def cache_user_data(user_id: int, timezone: str = "") -> None:
    """Save user_id and timezone to cache for reuse."""
    data = _read_cache()
    data["user_id"] = user_id
    if timezone:
        data["timezone"] = timezone
    _write_cache(data)


path: agents/openai.yaml
content: interface:
  display_name: "Bitrix24"
  short_description: "Bitrix24 CRM, tasks, chat, and MCP workflows"
  default_prompt: "Use $bitrix24 to work with Bitrix24 CRM, tasks, chat, calendar, drive, users, and MCP docs."
dependencies:
  tools:
    - type: "mcp"
      value: "bitrix24-docs"
      description: "Official Bitrix24 MCP documentation server"
      transport: "streamable_http"
      url: "https://mcp-dev.bitrix24.tech/mcp"
env:
  - BITRIX24_WEBHOOK_URL
policy:
  allow_implicit_invocation: true


path: references/sites.md
content: # Landing Pages and Sites

Use this file for creating and managing Bitrix24 sites and landing pages, adding blocks, editing content, and publishing.

Scope: `landing`

## Sites

- `landing.site.getList` — list sites (params: `select`, `filter`, `order`)
- `landing.site.add` — create a site (fields: `TITLE`, `CODE`, `DOMAIN_ID`)
- `landing.site.update` — update a site
- `landing.site.delete` — delete a site
- `landing.site.publication` — publish site and all its pages
- `landing.site.unpublic` — unpublish site and all its pages
- `landing.site.getPublicUrl` — get public URL of a site
- `landing.site.fullExport` — export site structure
- `landing.site.getFolders` — get site folders
- `landing.site.getRights` — get current user's rights to a site

## Pages

- `landing.landing.getList` — list pages (params: `select`, `filter`, `order`; filter by `SITE_ID`)
- `landing.landing.add` — add page (fields: `TITLE`, `CODE`, `SITE_ID`)
- `landing.landing.update` — update page
- `landing.landing.delete` — delete page
- `landing.landing.copy` — copy page
- `landing.landing.move` — move page to another site
- `landing.landing.publication` — publish a page
- `landing.landing.unpublic` — unpublish a page
- `landing.landing.getpublicurl` — get public URL of a page

Extra flags in `landing.landing.getList`: `get_preview=1`, `get_urls=1`, `check_area=1`.

## Blocks

Blocks are visual sections added to pages.

- `landing.block.getlist` — list blocks on a page
- `landing.block.getrepository` — list available block templates by section
- `landing.block.getContentFromRepository` — get block HTML before placing
- `landing.landing.addblock` — add block to page (fields: `CODE`, `AFTER_ID`, `ACTIVE`, `CONTENT`)
- `landing.block.updatenodes` — edit block content by node selectors
- `landing.block.updatecontent` — replace entire block HTML
- `landing.block.getcontent` — get block content (HTML, CSS, JS)
- `landing.block.getmanifest` — get block manifest (describes editable nodes)

Repository sections: `cover`, `about`, `text_image`, `columns`, `tiles`, `forms`, `team`, `gallery`, `menu`, `footer`, `social`, etc.

### Node types in `updatenodes`

- Text: `'.selector': 'New text with <b>html</b>'`
- Image: `'.selector': {src: '/path/image.png', alt: 'Alt text'}`
- Link: `'.selector': {text: 'Link text', href: 'https://...', target: '_blank'}`
- Embed: `'.selector': {src: '//youtube.com/embed/...', source: 'https://youtube.com/watch?v=...'}`

## Common Use Cases

### List all sites

```bash
python3 scripts/bitrix24_call.py landing.site.getList \
  --param 'params[select][]=ID' \
  --param 'params[select][]=TITLE' \
  --param 'params[select][]=DOMAIN.DOMAIN' \
  --json
```

### Create a site

```bash
python3 scripts/bitrix24_call.py landing.site.add \
  --param 'fields[TITLE]=My Site' \
  --param 'fields[CODE]=mysite' \
  --json
```

### Add a page to a site

```bash
python3 scripts/bitrix24_call.py landing.landing.add \
  --param 'fields[TITLE]=About Us' \
  --param 'fields[CODE]=about' \
  --param 'fields[SITE_ID]=292' \
  --json
```

### List available block templates

```bash
python3 scripts/bitrix24_call.py landing.block.getrepository \
  --param 'section=cover' \
  --json
```

### Add a block to a page

```bash
python3 scripts/bitrix24_call.py landing.landing.addblock \
  --param 'lid=351' \
  --param 'fields[CODE]=04.1.one_col_fix_with_title' \
  --json
```

### Edit block text content

```bash
python3 scripts/bitrix24_call.py landing.block.updatenodes \
  --param 'lid=351' \
  --param 'block=6058' \
  --param 'data[.landing-block-node-title]=Welcome to our site' \
  --param 'data[.landing-block-node-text]=We provide great services.' \
  --json
```

### Replace entire block HTML

```bash
python3 scripts/bitrix24_call.py landing.block.updatecontent \
  --param 'lid=351' \
  --param 'block=6058' \
  --param 'content=<section class="landing-block"><h1>Hello World</h1></section>' \
  --json
```

### Publish a site

```bash
python3 scripts/bitrix24_call.py landing.site.publication \
  --param 'id=292' \
  --json
```

### Get site public URL

```bash
python3 scripts/bitrix24_call.py landing.site.getPublicUrl \
  --param 'id[]=292' \
  --json
```

## Building a Site from Scratch

1. Create site: `landing.site.add`
2. Add pages: `landing.landing.add` with `SITE_ID`
3. Browse block templates: `landing.block.getrepository` by section
4. Add blocks to pages: `landing.landing.addblock` with block `CODE`
5. Edit block content: `landing.block.updatenodes` or `landing.block.updatecontent`
6. Publish: `landing.site.publication`
7. Get URL: `landing.site.getPublicUrl`

## Working Rules

- Landing methods use nested `params` for `select`/`filter`/`order` (not top-level).
- Block `CODE` comes from `landing.block.getrepository`.
- `landing.block.updatenodes` uses CSS selectors to target editable nodes.
- Get the block manifest first (`landing.block.getmanifest`) to see which nodes are editable.
- `landing.block.updatecontent` replaces the entire block HTML — use with care.
- Filter uses `%` for LIKE: `filter[TITLE]=%keyword%`.

## Good MCP Queries

- `landing site add getList publication`
- `landing landing add getList page`
- `landing block addblock getrepository updatenodes`
- `landing block updatecontent getcontent`


path: references/calendar.md
content: # Calendar

Use this file for personal calendars, group calendars, meetings, sections, recurrence, and availability checks.

## Core Methods

- `calendar.event.get` — list events in a date range (requires `type` and `ownerId`)
- `calendar.event.get.nearest` — list upcoming events (simplest way to get schedule)
- `calendar.event.getbyid` — get one event by ID
- `calendar.event.add` — create event
- `calendar.event.update` — update event
- `calendar.event.delete` — delete event
- `calendar.section.get` — list calendars (sections)
- `calendar.section.add` — create calendar
- `calendar.accessibility.get` — check user availability
- `calendar.meeting.status.get` — get current user's meeting participation status
- `calendar.resource.list` — list calendar resources

There is NO `calendar.get` or `calendar.list` method. Always use the full method names above.

## Critical: Required Parameters

`calendar.event.get` requires two mandatory parameters:

- `type` — one of: `user`, `group`, `company_calendar`
- `ownerId` — user ID for `user` type, group ID for `group`, `0` for `company_calendar`

Without these, the call fails with `ERROR_METHOD_NOT_FOUND` or similar.

`from` and `to` use date format `YYYY-MM-DD` (not datetime). Defaults: `from` = 1 month ago, `to` = 3 months ahead.

## Common Use Cases

### Show schedule via batch (preferred — one HTTP call)

Combine calendar + tasks in one request:

```bash
python3 scripts/bitrix24_batch.py \
  --cmd 'events=calendar.event.get?type=user&ownerId=1&from=2026-03-11&to=2026-03-11' \
  --cmd 'tasks=tasks.task.list?filter[RESPONSIBLE_ID]=1&filter[>=DEADLINE]=2026-03-11T00:00:00&filter[<=DEADLINE]=2026-03-11T23:59:59&select[]=ID&select[]=TITLE&select[]=DEADLINE&select[]=STATUS' \
  --json
```

### Show user's schedule for a specific day

Get user ID from cached config or `user.current`, then query events:

```bash
python3 scripts/bitrix24_call.py user.current --json
python3 scripts/bitrix24_call.py calendar.event.get \
  --param 'type=user' \
  --param 'ownerId=1' \
  --param 'from=2026-03-10' \
  --param 'to=2026-03-10' \
  --json
```

### Show upcoming events (easiest for "what's on my schedule")

```bash
python3 scripts/bitrix24_call.py calendar.event.get.nearest \
  --param 'type=user' \
  --param 'ownerId=1' \
  --param 'days=7' \
  --param 'forCurrentUser=true' \
  --json
```

### Check availability before scheduling

```bash
python3 scripts/bitrix24_call.py calendar.accessibility.get \
  --param 'users[]=1' \
  --param 'users[]=2' \
  --param 'from=2026-03-10' \
  --param 'to=2026-03-11' \
  --json
```

### Create an event

```bash
python3 scripts/bitrix24_call.py calendar.event.add \
  --param 'type=user' \
  --param 'ownerId=1' \
  --param 'name=Team Meeting' \
  --param 'from=2026-03-10T10:00:00' \
  --param 'to=2026-03-10T11:00:00' \
  --json
```

## Working Rules

- Always pass `type` and `ownerId` for `calendar.event.get`.
- Use `calendar.event.get.nearest` for "show my schedule" — it needs fewer parameters.
- Get user ID from `user.current` first if you don't know it.
- Check `calendar.accessibility.get` before proposing meeting slots.
- For read-only requests, execute immediately — do not ask permission.
- One retry on first failure, then report blocker.

## Good MCP Queries

- `calendar event get nearest`
- `calendar accessibility`
- `calendar section`
- `calendar resource booking`


path: references/chat.md
content: # Chat and Notifications

Use this file for messenger dialogs, chats, history, notifications, and file delivery into chats.

> **Important:** The methods below (`im.message.add`, `im.disk.file.commit`, etc.) are for managing chats on behalf of the webhook user — e.g., sending a message to an employee or posting a file to a group chat. Do **not** use them to reply to the user in the current conversation. Replies and file delivery to the user are handled automatically by the channel plugin.

> **Channels** (каналы / объявления) — see `references/channels.md`. Channels use the same `im.*` methods but with `ENTITY_TYPE=ANNOUNCEMENT` and `type: openChannel`.

## Separate `im.*` From `imbot.*`

Use `im.*` for normal IM REST methods (webhook-compatible):

- `im.message.add` — send message
- `im.message.update` / `im.message.delete`
- `im.message.share` — create entity (task/event/post) from a message
- `im.chat.add` / `im.chat.get` / `im.chat.update`
- `im.chat.user.add` / `im.chat.user.delete` / `im.chat.user.list`
- `im.dialog.get` / `im.dialog.messages.get`
- `im.dialog.messages.search` — search messages in a specific chat
- `im.dialog.users.list` — list dialog participants
- `im.dialog.read.all` — mark all chats as read
- `im.recent.list` / `im.recent.get`
- `im.dialog.writing` — typing indicator

Use `imbot.*` for bot scenarios (requires `CLIENT_ID`):

- `imbot.message.add` / `imbot.message.update` / `imbot.message.delete`
- `imbot.chat.add` / `imbot.dialog.get`
- `imbot.chat.sendTyping`

Do not mix `im.*` and `imbot.*` — pick the family that matches the integration.

## Notifications

- `im.notify.system.add` — system notification (app context only)
- `im.notify.personal.add` — personal notification (app context only)
- `im.notify.read` — mark notification as read

Important: `im.notify.system.add` and `im.notify.personal.add` work only through an application, not plain webhooks. If you get auth errors, this is likely the reason.

## Dialog Addressing

- `123` — direct dialog with user 123
- `chat456` — group chat 456
- `sg789` — group or project chat

## Common Use Cases

### Send a message to a chat

```bash
python3 scripts/bitrix24_call.py im.message.add \
  --param 'DIALOG_ID=chat42' \
  --param 'MESSAGE=Hello team' \
  --json
```

### Read dialog history

```bash
python3 scripts/bitrix24_call.py im.dialog.messages.get \
  --param 'DIALOG_ID=chat42' \
  --param 'LIMIT=20' \
  --json
```

### Send a Disk file to another chat

Use this to send a file to a specific chat on behalf of the webhook user (not for replying to the current conversation — the channel plugin handles that automatically).

```bash
python3 scripts/bitrix24_call.py im.disk.file.commit \
  --param 'CHAT_ID=42' \
  --param 'FILE_ID[]=5249' \
  --param 'MESSAGE=Project files' \
  --json
```

### Create a group chat

```bash
python3 scripts/bitrix24_call.py im.chat.add \
  --param 'TYPE=CHAT' \
  --param 'TITLE=Project discussion' \
  --param 'USERS[]=1' \
  --param 'USERS[]=2' \
  --json
```

### Search messages in a chat

```bash
python3 scripts/bitrix24_call.py im.dialog.messages.search \
  --param 'CHAT_ID=42' \
  --param 'SEARCH_MESSAGE=contract' \
  --param 'LIMIT=20' \
  --json
```

Supports date filters: `DATE_FROM`, `DATE_TO` (ISO 8601), `DATE` (single day).
Search string must be longer than 2 characters. Returns messages, users, and files.

### Create task from a chat message

```bash
python3 scripts/bitrix24_call.py im.message.share \
  --param 'MESSAGE_ID=34261' \
  --param 'DIALOG_ID=chat42' \
  --param 'TYPE=TASK' \
  --json
```

`TYPE` values: `TASK` (task), `CALEND` (calendar event), `POST` (feed post), `CHAT` (forward to chat).
Get `MESSAGE_ID` from `im.dialog.messages.get` or `im.dialog.messages.search`.

### Mark all chats as read

```bash
python3 scripts/bitrix24_call.py im.dialog.read.all --json
```

No parameters needed. Marks all dialogs as read for the current user.

## `CLIENT_ID` for Bots

For `imbot.*` methods:

- Provide `CLIENT_ID` when registering the bot
- Persist it and reuse in every `imbot.*` call
- Treat `CLIENT_ID` as a secret

## Formatting

Bitrix24 chat uses BB-code. Do not double-convert if Markdown is already converted to BB-code.

## Good MCP Queries

- `im message add chat`
- `im message share`
- `im dialog messages search`
- `im dialog read all`
- `imbot message`
- `im dialog messages get`
- `im disk file commit`


path: references/troubleshooting.md
content: # Troubleshooting

Use this file when webhook calls fail or the agent cannot reach the portal.

## Default Behavior

Do the first diagnosis yourself before asking the user anything.

Preferred order:

1. Run `scripts/check_webhook.py --json`
2. Inspect the result: format, DNS, HTTP status
3. Report concrete findings and one next fix

## Typical Failure: No Webhook Configured

If `check_webhook.py` reports `"source": "missing"`:

Tell the user once: "Webhook не настроен. Попросите администратора указать его в настройках."
Do not ask the user to paste a webhook URL. Do not retry.

## Typical Failure: DNS Resolution Failed

Usually means:

- typo in the portal domain
- local network issue

Tell the user the portal address could not be reached. Name the host that failed.

## Typical Failure: Bad Format

Expected format:

```text
https://your-portal.bitrix24.ru/rest/<user_id>/<webhook>/
```

Common mistakes:

- copied portal URL instead of webhook URL
- extra quotes or spaces
- wrong user ID segment

## Typical Failure: HTTP 401 or Auth Errors

Usually indicates:

- revoked webhook
- wrong secret
- expired OAuth token

Ask the user to verify or regenerate the webhook in Bitrix24.

## Typical Failure: `ACCESS_DENIED` or `insufficient_scope`

Missing permissions. Tell the user exactly which scope family is likely missing:

- CRM
- Tasks
- Calendar
- Disk
- IM
- `imbot`

Do not just say "permissions issue" without naming the likely scope.

## User-Facing Style

Prefer:

- what you checked
- what failed
- what is already confirmed working
- one next action
- plain business language ("connection to Bitrix24", "access to calendar")
- doing the next safe step yourself before asking the user

Avoid:

- long lists of shell commands for the user
- asking for confirmation before a simple retry
- exposing webhook URLs or secrets
- talking about curl, MCP, JSON, DNS, or config mechanics unless explicitly asked
- multiple-choice menus

## Response Templates

Bad:

- "What you need to do now: 1. create env 2. source env 3. run curl 4. or try direct URL..."

Better for missing webhook:

- "Доступ к Битрикс24 не настроен. Попросите администратора указать вебхук в настройках."

Better when DNS failed:

- "Не удаётся связаться с Битрикс24. Похоже, адрес портала указан неверно."

Better when auth failed:

- "Связь с Битрикс24 есть, но доступ не подтверждён. Скорее всего, вебхук нужно обновить."

## Autonomous Retry Rule

For safe read-only requests:

- Execute immediately
- If it fails, run `check_webhook.py --json`
- If fixable, retry once automatically
- Only then report the blocker

Do not ask "Should I try again?" — just do it.


path: references/smartprocess.md
content: # Smart Processes and Funnels

Use this file for smart processes (custom CRM types), funnels/categories, stages, and stage history.

Scope: `crm`

## Entity Type IDs

Standard types:

- `1` — lead
- `2` — deal
- `3` — contact
- `4` — company
- `5` — old invoice (deprecated)
- `7` — quote
- `31` — new invoice (smart invoice)
- `128+` — custom smart processes

Discover custom types with `crm.type.list`.

## Smart Process Types

- `crm.type.list` — list all smart process types (filter by `title`, `entityTypeId`, `isCategoriesEnabled`, etc.)
- `crm.type.get` — get one type by `id`
- `crm.type.add` — create a new smart process type
- `crm.type.update` — update a smart process type
- `crm.type.delete` — delete a smart process type

## Items (Universal API)

`crm.item.*` works for deals, leads, contacts, companies, quotes, invoices, and all custom smart processes. The `entityTypeId` parameter is always required.

- `crm.item.list` — list items (requires `entityTypeId`, supports `select`, `filter`, `order`)
- `crm.item.get` — get one item by `id` and `entityTypeId`
- `crm.item.add` — create item (requires `entityTypeId` and `fields`)
- `crm.item.update` — update item
- `crm.item.delete` — delete item
- `crm.item.fields` — get field schema for an entity type

Filter operators are prefixes on the key: `>=dateCreate`, `!stageId`, `%title`.

## Funnels (Categories)

- `crm.category.list` — list funnels for a type (requires `entityTypeId`)
- `crm.category.get` — get one funnel
- `crm.category.add` — create funnel
- `crm.category.update` — update funnel
- `crm.category.delete` — delete funnel
- `crm.category.fields` — field schema

## Stages and Statuses

- `crm.status.list` — list statuses/stages (filter by `ENTITY_ID`)
- `crm.stagehistory.list` — stage change history (requires `entityTypeId`)

Stage history `TYPE_ID` values:

- `1` — create
- `2` — intermediate stage change
- `3` — final stage
- `5` — funnel change

`STAGE_SEMANTIC_ID`: `P` = in progress, `S` = success, `F` = fail.

## Common Use Cases

### List all smart process types

```bash
python3 scripts/bitrix24_call.py crm.type.list --json
```

### List items of a smart process

```bash
python3 scripts/bitrix24_call.py crm.item.list \
  --param 'entityTypeId=128' \
  --param 'select[]=id' \
  --param 'select[]=title' \
  --param 'select[]=stageId' \
  --param 'select[]=assignedById' \
  --json
```

### Create an item in a smart process

```bash
python3 scripts/bitrix24_call.py crm.item.add \
  --param 'entityTypeId=128' \
  --param 'fields[title]=New item' \
  --param 'fields[assignedById]=1' \
  --json
```

### Get field schema for a type

```bash
python3 scripts/bitrix24_call.py crm.item.fields \
  --param 'entityTypeId=128' \
  --json
```

### List funnels (categories) for deals

```bash
python3 scripts/bitrix24_call.py crm.category.list \
  --param 'entityTypeId=2' \
  --json
```

### Get stage history

```bash
python3 scripts/bitrix24_call.py crm.stagehistory.list \
  --param 'entityTypeId=2' \
  --param 'filter[>=CREATED_TIME]=2026-01-01T00:00:00' \
  --json
```

## Working Rules

- `entityTypeId` is mandatory for all `crm.item.*` and `crm.category.*` methods.
- Use `crm.type.list` to discover `entityTypeId` values for custom processes.
- Fields use camelCase in `crm.item.*` (e.g. `stageId`, `assignedById`, `categoryId`).
- For deal-specific methods (`crm.deal.*`), fields use UPPER_CASE (`STAGE_ID`, `CATEGORY_ID`).
- Always call `crm.item.fields` before writing custom field values.

## Good MCP Queries

- `crm type list add smart process`
- `crm item list add fields`
- `crm category list funnels`
- `crm stagehistory`
- `crm status list`


path: references/drive.md
content: # Drive and Disk

Use this file for storage, folder, file, and external-link operations.

## Core Methods

Storages:

- `disk.storage.getlist` — list all storages
- `disk.storage.get` — get storage by ID
- `disk.storage.getchildren` — list root contents
- `disk.storage.addfolder` — create folder in root
- `disk.storage.uploadfile` — upload file to root

Folders:

- `disk.folder.get` — folder metadata
- `disk.folder.getchildren` — list folder contents
- `disk.folder.addsubfolder` — create subfolder
- `disk.folder.uploadfile` — upload file to folder
- `disk.folder.getexternallink` — public link
- `disk.folder.moveto` — move folder
- `disk.folder.deletetree` — delete folder permanently

Files:

- `disk.file.get` — file metadata (includes `DOWNLOAD_URL`)
- `disk.file.copyto` — copy to folder
- `disk.file.moveto` — move to folder
- `disk.file.rename` — rename file
- `disk.file.uploadversion` — upload new version
- `disk.file.delete` — delete permanently

Attached objects (files linked to tasks, chats, CRM entities):

- `disk.attachedObject.get` — get info about an attached file (from task, chat, etc.)

Chat handoff:

- `im.disk.file.commit` — send existing Disk file to chat

## Common Use Cases

### List all storages

```bash
python3 scripts/bitrix24_call.py disk.storage.getlist --json
```

### Browse storage contents

```bash
python3 scripts/bitrix24_call.py disk.storage.getchildren \
  --param 'id=1' \
  --json
```

### Browse folder contents

```bash
python3 scripts/bitrix24_call.py disk.folder.getchildren \
  --param 'id=42' \
  --json
```

### Get file metadata

```bash
python3 scripts/bitrix24_call.py disk.file.get \
  --param 'id=9043' \
  --json
```

### Upload file to a folder

```bash
python3 scripts/bitrix24_call.py disk.folder.uploadfile \
  --param 'id=42' \
  --param 'data[NAME]=document.txt' \
  --param 'fileContent[0]=document.txt' \
  --param 'fileContent[1]=<base64_content>' \
  --json
```

### Get public link for a folder

```bash
python3 scripts/bitrix24_call.py disk.folder.getexternallink \
  --param 'id=42' \
  --json
```

### Get attached file info (from task, chat, etc.)

```bash
python3 scripts/bitrix24_call.py disk.attachedObject.get \
  --param 'id=495' \
  --json
```

Returns `OBJECT_ID` (disk file ID), `NAME`, `SIZE`, `DOWNLOAD_URL`, `MODULE_ID`, `ENTITY_TYPE`, `ENTITY_ID`.
Get the attachment ID from methods like `tasks.task.get` (UF_TASK_WEBDAV_FILES) or chat message files.

## Working Rules

- Find file IDs from `disk.storage.getchildren` or `disk.folder.getchildren`.
- Use `disk.file.get` to inspect metadata before download or move.
- `DOWNLOAD_URL` requires authentication — it's not a public link.
- Use `disk.folder.getexternallink` for sharing.
- Use `disk.attachedObject.get` to get info about files attached to tasks or messages.

## Good MCP Queries

- `disk storage folder file`
- `disk file upload version`
- `disk external link`
- `disk attachedObject get`
- `im disk file commit`


path: references/mcp-workflow.md
content: # MCP Workflow

Use MCP as the source of truth for Bitrix24 docs.

## Connection Facts

- Endpoint: `https://mcp-dev.bitrix24.tech/mcp`
- This is a streamable MCP endpoint, not a regular documentation page
- Without `Accept: text/event-stream`, the endpoint rejected requests with `406 Not Acceptable`
- `initialize` reported:
  - protocol version `2025-03-26`
  - server `b24-dev-mcp`
  - version `0.2.0`

## Recommended Order

1. Search.
2. Resolve the exact method, event, or article title.
3. Fetch details.
4. Only then make the REST call or write the example.

## Search Tool

Use `bitrix-search` first.

Helpful arguments:

- `query`: natural-language search text
- `limit`: cap the number of matches
- `doc_type`: narrow the result space

Supported `doc_type` values confirmed from MCP:

- `method`
- `event`
- `other`
- `app_development_docs`

## Detail Tools

- `bitrix-method-details`: exact REST method name only
- `bitrix-event-details`: exact event name only
- `bitrix-article-details`: exact non-method article title
- `bitrix-app-development-doc-details`: exact app-development title

## Practical Search Patterns

Use queries close to the user task:

- `crm deal add`
- `task checklist`
- `task comment`
- `im message chat`
- `calendar event section`
- `disk storage folder file`
- `user department structure`
- `oauth install callback`

Then pass the exact name from search into the appropriate details tool.

## Patterns Confirmed During Analysis

The current MCP server returned strong matches for these families:

- CRM: `crm.deal.*`, `crm.contact.*`, `crm.company.*`, `crm.lead.*`, `crm.activity.*`, `crm.item.*`
- Tasks: `tasks.task.*`, `task.checklistitem.*`, `task.commentitem.*`, `task.planner.getlist`
- Chat: `im.message.*`, `im.chat.*`, `im.dialog.*`, `im.recent.*`, `im.notify.*`, `imbot.*`
- Calendar: `calendar.event.*`, `calendar.section.*`, `calendar.accessibility.get`
- Drive: `disk.storage.*`, `disk.folder.*`, `disk.file.*`, `im.disk.file.commit`
- Users and org structure: `user.*`, `department.*`, `im.department.*`, `im.search.*`

## Avoid Stale Memory

Do not trust memory alone when:

- a method family is large
- a scenario is scope-sensitive
- a method may be deprecated
- an example mixes `im.*` and `imbot.*`
- a field name is portal-specific

Re-run `bitrix-method-details` in those cases.


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)