Таска
На главную
Документация для разработчиков

API Таски v1.0

Один GraphQL-эндпоинт, один ключ доступа в заголовке. Позволяет сторонним сервисам полностью управлять содержимым пространства: проектами, досками, статусами, задачами, чек-листами, тегами, полями, эпиками и базой знаний.

Обзор

POST http://taska.be/api/v1/graphql

API построено на GraphQL: все операции идут одним POST-запросом на адрес выше. Тело запроса — JSON с полями query и (необязательно) variables.

Ключевые свойства, которые стоит знать до начала работы:

Ключ привязан к одному пространству

Пространство выбирается в момент выпуска ключа и не меняется. Всё, что видит и меняет ключ, находится внутри этого пространства — обратиться к соседнему нельзя даже зная идентификаторы объектов.

Ключ действует от имени человека

Это тот, кто его выпустил. Все проверки доступа к проектам работают ровно так же, как если бы этот пользователь зашёл в интерфейс.

Изменения видны мгновенно

Всё, что API создаёт и меняет, рассылается в открытые вкладки Таски по вебсокету — пользователю не нужно перезагружать страницу.

Правила приложения соблюдаются

Лимиты тарифа, платные функции, права ролей, автоматическая смена исполнителя при переходе между статусами, правила повторения задач — всё работает так же, как в интерфейсе. API не является обходным путём.

Ключи доступа

Ключ выпускается в интерфейсе Таски. Где именно — зависит от того, к какому пространству нужен доступ:

ПространствоГде выпуститьКто может
ЛичноеПрофиль → вкладка APIВладелец аккаунта
КомандноеНастройки пространства → вкладка APIВладелец и администраторы пространства

Ключ выглядит так:

taska_k3f9a1c7b204_x8Qm2LpZv...

Он состоит из трёх частей: префикса taska, публичного идентификатора ключа и секрета. Публичный идентификатор виден в интерфейсе и в журнале обращений — по нему удобно понять, какая именно интеграция сделала запрос.

Секрет показывается ровно один раз — сразу после создания или ротации ключа. В базе Таски он не хранится, только его необратимый хеш. Если ключ потерян, восстановить его невозможно: нажмите «Перевыпустить» — старый секрет перестанет работать немедленно, а настройки, права и статистика ключа сохранятся.

Авторизация

Ключ передаётся в заголовке Authorization:

Authorization: Bearer taska_k3f9a1c7b204_x8Qm2LpZv...

Если ваш прокси или платформа перехватывает Authorization, используйте запасной заголовок — он равнозначен:

X-Taska-Api-Key: taska_k3f9a1c7b204_x8Qm2LpZv...

Проверить ключ можно запросом viewer — он не требует никаких прав и возвращает контекст, в котором работает ключ:

query {
  viewer {
    tokenName
    tokenPrefix
    scopes
    spaceId
    spaceTitle
    actingUserEmail
    expiresAt
    rateLimitPerMinute
  }
}

Когда ключ перестаёт работать

Запрос вернёт 401 с кодом unauthenticated, если:

  • ключ отозван или удалён;
  • истёк заданный при выпуске срок действия;
  • запрос пришёл с адреса вне белого списка IP (если список задан);
  • пользователь, от имени которого выпущен ключ, покинул пространство или был из него исключён. Это проверяется на каждом запросе: отключение сотрудника автоматически закрывает его интеграции, без ручной чистки ключей.

Быстрый старт

curl
curl -s http://taska.be/api/v1/graphql \
  -H "Authorization: Bearer $TASKA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"query": "{ projects { totalCount items { id title type } } }"}'
JavaScript
async function taska(query, variables = {}) {
  const response = await fetch('http://taska.be/api/v1/graphql', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.TASKA_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ query, variables }),
  });

  const payload = await response.json();

  // GraphQL отвечает 200 даже на прикладные ошибки — проверяем тело ответа,
  // а не только HTTP-статус.
  if (payload.errors?.length) {
    const first = payload.errors[0];
    throw new Error(`[${first.extensions?.code}] ${first.message}`);
  }

  return payload.data;
}

const { createTask } = await taska(`
  mutation NewTask($statusId: ID!, $title: String!) {
    createTask(statusId: $statusId, title: $title) {
      task { id title status { title } }
    }
  }
`, { statusId: '42', title: 'Задача из внешнего сервиса' });
Python
import os
import requests

ENDPOINT = "http://taska.be/api/v1/graphql"
SESSION = requests.Session()
SESSION.headers["Authorization"] = f"Bearer {os.environ['TASKA_API_KEY']}"


def taska(query: str, **variables):
    response = SESSION.post(ENDPOINT, json={"query": query, "variables": variables})
    payload = response.json()

    if payload.get("errors"):
        error = payload["errors"][0]
        code = (error.get("extensions") or {}).get("code")
        raise RuntimeError(f"[{code}] {error['message']}")

    return payload["data"]


data = taska(
    """
    query OpenTasks($limit: Int) {
      tasks(filter: {isCompleted: false}, orderBy: DEADLINE, orderDirection: ASC, limit: $limit) {
        totalCount
        items { id title deadline performer { fullName } }
      }
    }
    """,
    limit=50,
)

Права доступа

При выпуске ключа выбирается набор прав. Каждая операция требует конкретного права; при его отсутствии запрос вернёт ошибку с кодом forbidden и подсказкой, чего именно не хватает.

Права на чтение и на запись независимы: tasks:write не включает tasks:read. Это сделано специально — так можно выдать ключ, который только создаёт задачи из внешней системы, но не может выгрузить содержимое пространства.

ПравоЧто открывает
space:readЧтение данных пространства
members:readЧтение списка участников пространства
projects:readЧтение проектов, шаблонов, десков и коннекторов
projects:writeСоздание, изменение, архивация и удаление проектов
boards:readЧтение досок, статусов, тегов, полей задач и эпиков
boards:writeИзменение структуры досок, статусов, тегов, полей и эпиков
tasks:readЧтение задач, чек-листов, значений полей и файлов
tasks:writeСоздание и изменение задач, чек-листов и значений полей
knowledge:readЧтение хранилищ, папок и страниц базы знаний
knowledge:writeСоздание и изменение хранилищ, папок и страниц
Выдавайте минимум. Если сервис только строит отчёты — хватит прав на чтение. Отозвать лишнее право у работающей интеграции всегда сложнее, чем добавить нужное.

Ошибки

Ошибки приходят в стандартном для GraphQL виде. Ориентируйтесь на extensions.code — это стабильный машиночитаемый идентификатор. Текст message предназначен человеку и может меняться.

{
  "data": null,
  "errors": [
    {
      "message": "Ключу не хватает права «tasks:write». Выданные права: tasks:read.",
      "extensions": {
        "code": "forbidden",
        "required_scope": "tasks:write",
        "granted_scopes": ["tasks:read"]
      }
    }
  ]
}
КодHTTPЧто означает и что делать
unauthenticated401Ключ не передан, неверен, отозван, истёк, либо у его пользователя больше нет доступа к пространству. Повтор не поможет — нужен новый ключ.
forbidden200 / 403Ключу не хватает права, либо у пользователя нет полномочий на операцию (например, гость не создаёт проекты). Добавьте право в настройках ключа.
not_found200Объекта нет либо он вне пространства ключа. Эти два случая намеренно неразличимы.
invalid_input200 / 400Аргументы не прошли валидацию. Сообщение содержит список допустимых значений.
conflict200Операция противоречит текущему состоянию: правка архивного проекта, удаление непустой колонки, дубль связи.
quota_exceeded200Исчерпан лимит тарифа (проекты, доски, статусы). В extensions есть limit, current, max.
feature_not_allowed200Функция недоступна на текущем тарифе пространства — например, платный блок статьи.
rate_limited429Превышена частота запросов. Заголовок Retry-After подскажет, через сколько секунд повторить.
query_too_complex400Слишком глубокая вложенность запроса. Разбейте на несколько.
internal_error200 / 500Ошибка на нашей стороне. Имеет смысл повторить с экспоненциальной задержкой; если повторяется — напишите в поддержку и укажите время запроса.
Обратите внимание: HTTP-статус 200 не означает успех. GraphQL отдаёт 200 для большинства прикладных ошибок. Всегда проверяйте наличие поля errors в теле ответа — как в примерах выше.

Пагинация

Все списки устроены одинаково: аргументы limit и offset, ответ одной формы.

query Page($offset: Int!) {
  tasks(limit: 100, offset: $offset) {
    items { id title }
    totalCount   # всего под фильтр
    hasMore      # есть ли следующая страница
    limit        # фактически применённый размер страницы
    offset
  }
}

Значение limit по умолчанию — 50, максимум — 200. Запрос большего значения не даст ошибки: он будет тихо уменьшен до максимума, а фактический размер вернётся в поле limit. Проверяйте его, если нарезаете выгрузку по страницам.

Полная выгрузка выглядит так:

async function fetchAll(query, variables = {}) {
  const all = [];
  let offset = 0;

  for (;;) {
    const page = (await taska(query, { ...variables, offset })).tasks;
    all.push(...page.items);
    if (!page.hasMore) break;
    // Шагаем на фактический limit, а не на запрошенный — сервер мог его урезать.
    offset += page.limit;
  }

  return all;
}

Лимиты и ограничения

Частота запросов

Лимит задаётся для каждого ключа отдельно (по умолчанию 120 запросов в минуту). Мутация расходует бюджет как два обычных запроса — она пишет в базу и рассылает уведомления.

Каждый ответ содержит текущее состояние счётчика:

X-RateLimit-Limit: 120
X-RateLimit-Remaining: 96
X-RateLimit-Reset: 34

При превышении вернётся 429 и заголовок Retry-After. Правильная реакция — подождать указанное число секунд, а не повторять сразу.

Сложность запроса

Максимальная вложенность — 12 уровней. Связи в схеме цикличны (проект → доски → статусы → доска → проект), и без ограничения одним запросом можно было бы вытянуть всю базу.

Лимиты тарифа

Количество проектов, досок и статусов ограничено тарифом пространства ровно так же, как в интерфейсе. При исчерпании вернётся quota_exceeded с деталями.

Безопасность

Ключ — это пароль

Храните его в переменных окружения или секрет-хранилище. Не коммитьте в репозиторий, не кладите во фронтенд-код и не передавайте в query-параметрах URL — они попадают в логи прокси.

Отдельный ключ на интеграцию

Тогда утечка или сбой одного сервиса закрывается ротацией одного ключа, а по журналу видно, кто что делал.

Белый список IP

Если интеграция ходит с фиксированных адресов, укажите их при выпуске ключа — украденный ключ станет бесполезен вне вашей сети. Поддерживаются отдельные адреса и подсети (203.0.113.0/24).

Срок жизни

Для временных задач (миграция, разовая выгрузка) задавайте срок в днях — ключ отключится сам.

Журнал обращений

Мутации и любые отказы записываются. Историю последних обращений видно в карточке ключа.

Модель данных

Иерархия сущностей внутри пространства:

Space (пространство — привязано к ключу)
├── Member                     участники
└── Project                    проект
    ├── Board                  канбан-доска
    │   └── Status             колонка доски
    │       └── Task           задача
    │           ├── Todo       пункт чек-листа
    │           ├── Tag        тег
    │           └── FieldValue значение пользовательского поля
    ├── TaskField              описание поля задач проекта
    ├── Epic                   эпик (строка диаграммы Гантта)
    │   └── EpicDependency     зависимость между эпиками
    └── Storage                хранилище базы знаний
        ├── Folder             папка (может быть вложенной)
        └── Page               страница

Полезные детали:

  • Задача может существовать вне доски — во «входящих» пространства. Такие задачи ищутся фильтром { inInbox: true }.
  • Тип проекта (simple, complex, storage, roadmap) определяет, что создаётся автоматически. У simple сразу появляется доска с набором статусов, у storage — хранилище базы знаний.
  • Поля задач бывают системными (у них заполнен builtinKey: описание, исполнитель, статус, приоритет, дедлайн, теги, эпик) и пользовательскими. Системные нельзя удалять и переименовывать — только скрывать и переставлять.
  • Допустимые значения всех перечислимых полей (приоритеты, цвета, иконки, типы) отдаёт запрос metadata. Запросите его один раз при настройке интеграции.

Блочный редактор

Страница базы знаний бывает трёх типов, и тип задаётся один раз, при создании (pageType): wysiwyg — форматированный HTML, markdown — обычный markdown, blocks — блочный редактор. Последний и есть основной формат статей: из него собираются таблицы, диаграммы, колонки, чек-листы, карточки задач, живые выборки задач и всё остальное, чего в тексте выразить нельзя.

Содержимое блочной страницы лежит в том же поле content, но это строка с JSON-документом. API принимает её как есть и не разбирает: формат — контракт редактора, и вся ответственность за него на том, кто пишет. Поэтому ниже он описан целиком.

Документ

{
  "version": 1,
  "blocks": [
    { "id": "blk_Qa7vMx0Lp2", "type": "heading",   "data": { "level": 1, "spans": [ { "text": "Регламент" } ] } },
    { "id": "blk_8fKd1sZbNe", "type": "paragraph", "data": { "spans": [ { "text": "Как обрабатывать заявки." } ] } }
  ]
}
  • id — стабильный идентификатор блока, blk_ + 10 символов base62. По нему адресуются правки, якоря оглавления и ссылка диаграммы на таблицу, поэтому уникальность обязательна: копируя блок, выдайте копии новый id.
  • type — тип блока из таблицы ниже. Неизвестный тип редактор превращает в пустой абзац: содержимое такого блока теряется молча.
  • data — поля блока. Набор зависит от типа; отсутствующие поля дополняются значениями по умолчанию при чтении.
  • version — версия формата документа. Сейчас всегда 1.

Текст: сегменты и метки

Форматированный текст — это не HTML, а массив сегментов. Каждый сегмент — кусок текста с набором меток:

"spans": [
  { "text": "Обычный текст, " },
  { "text": "жирный", "marks": [ { "type": "bold" } ] },
  { "text": " и ссылка", "marks": [ { "type": "link", "href": "https://taska.be" } ] }
]
typeПоляЧто означает
bold, italic, underline, strike, codeНачертание. Комбинируются свободно.
color, bgColorvalueКлюч палитры: emerald, green, lime, yellow, amber, orange, red, scarlet, pink, fuchsia, purple, violet, indigo, blue, sky, cyan, teal, slate, gray, zinc, silver, stone. Не CSS-цвет: оттенок берётся из темы.
linkhrefВнешняя ссылка. Разрешены схемы http, https, mailto, tel, ftp и относительные пути; всё остальное (в первую очередь javascript:) отбрасывается при чтении.
pageLinkpageId, snapshotСсылка на другую страницу базы знаний. По этим меткам сервер пересобирает граф связей страницы, поэтому ссылаться можно только на страницы того же хранилища. snapshot — название на момент вставки, оно останется в тексте, даже если страницу переименуют.
mentionuserId, snapshotУпоминание участника. Идентификаторы берите в запросе members. Упомянутый получит уведомление — ровно один раз на каждое появление в статье, как и при упоминании из интерфейса. Упоминание того, кто не состоит в пространстве, игнорируется.
commentthreadIdПривязка фрагмента к ветке обсуждения. Сам текст комментариев живёт на сервере, в документе только метка.
Метки link, pageLink и mention помечают цельную сущность, а не оформление: они относятся ко всему сегменту целиком. Остальные метки можно накладывать на любые куски текста и комбинировать между собой.

Кроме плоского spans у некоторых блоков есть отдельные текстовые поля — caption, title, body, summary. Все они устроены одинаково: объект { "spans": [ … ] }.

Типы блоков

typeБлокПоля data
paragraphАбзацspans
headingЗаголовокlevel (1–6, по умолчанию 2), spans
quoteЦитатаspans, caption
codeКодcode (плоский текст, без меток), language (id языка highlight.js или auto), filename, wrap, lineNumbers
dividerРазделительstyle: solid / stars (по умолчанию) / dashed
linkСсылка-карточкаhref, label
imageКартинкаslug, alt, caption, width, height, align (left / center / right)
embedВстройкаurl, height (160–1200), caption
filesФайлыview (grid / list), folders[], items[], order[]
warningВыноскаkind (info / warn / danger), title, body
listСписокstyle (bullet / numbered), items[]: id, spans, level
checklistЧек-листitems[]: id, checked, spans, level
tableТаблицаwidths[], rows[]: id, cells[]: id, spans, align, valign; headerRow, headerCol
chartДиаграммаsourceId (id блока-таблицы), kind (bar / horizontalBar / line / area / pie / doughnut / radar), labelCol, valueCols[], height (160–900), legend, stacked, caption
tocОглавлениеmaxLevel (2–6), mode (full / left / center / right), numbered
layoutКолонкиcolumns (2 или 3), cells[][] — по списку блоков на колонку, ratios[]
toggleСворачиваемая секцияsummary, open, cells[][] — ровно один вложенный список блоков
taskКарточка задачиtaskId
taskListВыборка задачboardId, statusIds[], performerIds[], tagIds[], epicId, state (open / closed / all), limit (1–100), title
syncedПереиспользуемый фрагментfragmentId

Что важно знать про отдельные блоки:

  • Диаграмма своих цифр не хранит. Она ссылается полем sourceId на блок-таблицу в этой же статье, а labelCol и valueCols — индексы её колонок (с нуля). Сначала вставьте таблицу, потом диаграмму с её id.
  • Оглавление вычисляется на лету из заголовков статьи — записывать в него пункты не нужно и некуда.
  • Карточка и выборка задач — живые. В документе лежат только идентификаторы и фильтры; названия, статусы и исполнители подтягиваются при открытии статьи.
  • Картинки и файлы должны быть уже загружены. В документе хранится slug файла, а загрузка идёт не через GraphQL. Возьмите slug уже загруженного файла запросом files.
  • Встраиваются только известные сервисы — Figma, Miro, YouTube, RUTUBE, VK Видео, Vimeo, Loom, документы Google и Яндекса, CodePen. Прочие ссылки блок покажет карточкой: почти все сайты запрещают встраивать себя во фрейм.
  • Код — плоский текст. Подсветка вычисляется при рендере, поэтому меток внутри code нет и быть не может.
  • Переиспользуемый фрагмент — отдельная сущность хранилища: один и тот же текст, показанный в нескольких статьях. В публичном API его нет, в документе хранится только fragmentId.

Вложенность

Вкладывать блоки умеют ровно два типа — layout (2–3 колонки) и toggle (одна сворачиваемая секция). Оба хранят вложенное в data.cells — массиве списков блоков того же формата, что и blocks документа:

{
  "id": "blk_Lc93pQmT1x",
  "type": "layout",
  "data": {
    "columns": 2,
    "ratios": [1, 1],
    "cells": [
      [ { "id": "blk_a1", "type": "paragraph", "data": { "spans": [ { "text": "Слева" } ] } } ],
      [ { "id": "blk_b2", "type": "paragraph", "data": { "spans": [ { "text": "Справа" } ] } } ]
    ]
  }
}
  • Колонки нельзя вкладывать в колонки, а в сворачиваемую секцию — ни колонки, ни другую секцию.
  • Длина cells равна columns; у toggle ячейка всегда одна.
  • Пустая ячейка при чтении получает пустой абзац — иначе в неё некуда поставить курсор.
  • Вложенность пунктов списка — это не дерево, а поле level у пункта (0–4).

Ограничения

ЧтоПределЧто будет при превышении
Размер документа1 МБРедактор откажется сохранять статью — разбейте её на несколько.
Таблица10 колонок, 100 строкЛишнее отбрасывается при чтении.
Вложенность пунктов списка4Уровень обрезается.
Высота встройки160–1200Значение зажимается в диапазон.
Высота диаграммы160–900Значение зажимается в диапазон.
Блоки chart, files, task и taskList — платная функция. Добавить новый такой блок на бесплатном тарифе нельзя: запись вернёт ошибку feature_not_allowed. Уже стоящие в статье блоки при этом никуда не деваются — их видно, а текст вокруг них правится как обычно, поэтому сохранять статью с ними API не мешает. Решает тариф проекта, которому принадлежит хранилище статьи, а не подписка владельца ключа.

Как писать блочную статью

Содержимое передаётся строкой, поэтому документ удобнее собрать объектом и сериализовать в переменную запроса — экранировать JSON внутри JSON руками не нужно:

mutation NewArticle($storageId: ID!, $content: String!) {
  createPage(
    storageId: $storageId
    title: "Регламент обработки заявок"
    pageType: "blocks"
    content: $content
  ) {
    page { id title pageType }
  }
}
переменные запроса: content — это JSON.stringify(документа)
{
  "storageId": "4",
  "content": "{\"version\":1,\"blocks\":[ … ]}"
}

Небольшая статья целиком — заголовок, текст, таблица и диаграмма по ней:

{
  "version": 1,
  "blocks": [
    { "id": "blk_h1", "type": "heading",   "data": { "level": 1, "spans": [ { "text": "Выручка за квартал" } ] } },
    { "id": "blk_p1", "type": "paragraph", "data": { "spans": [
        { "text": "Подробности — в " },
        { "text": "соседней статье", "marks": [ { "type": "pageLink", "pageId": "31", "snapshot": "Методика расчёта" } ] },
        { "text": ". Ответственный: " },
        { "text": "Иван", "marks": [ { "type": "mention", "userId": "7", "snapshot": "Иван Петров" } ] }
      ] } },
    { "id": "blk_t1", "type": "table", "data": {
        "widths": [1, 1],
        "headerRow": true,
        "headerCol": false,
        "rows": [
          { "id": "row_1", "cells": [
              { "id": "c11", "spans": [ { "text": "Месяц" } ],   "align": "left",  "valign": "top" },
              { "id": "c12", "spans": [ { "text": "Выручка" } ], "align": "right", "valign": "top" } ] },
          { "id": "row_2", "cells": [
              { "id": "c21", "spans": [ { "text": "Январь" } ],  "align": "left",  "valign": "top" },
              { "id": "c22", "spans": [ { "text": "120" } ],     "align": "right", "valign": "top" } ] }
        ] } },
    { "id": "blk_c1", "type": "chart", "data": {
        "sourceId": "blk_t1", "kind": "bar", "labelCol": 0, "valueCols": [1],
        "height": 320, "legend": true, "stacked": false,
        "caption": { "spans": [ { "text": "Помесячно, тыс. ₽" } ] } } }
  ]
}

Как править существующую статью

Гранулярных операций над блоками в публичном API нет: правка — это «прочитать content, изменить нужный блок, записать документ целиком». Блок ищите по id, а не по позиции: позиции меняются при каждом редактировании в интерфейсе.

query Read($id: ID!) {
  page(id: $id) { id pageType content updatedAt }
}

mutation Write($id: ID!, $content: String!) {
  updatePage(id: $id, content: $content) { page { id updatedAt } }
}
Запись заменяет документ целиком, а редактор автосохраняет статью буквально на каждый абзац. Если в этот момент статья открыта у человека, победит тот, кто записал последним. Для длинных статей читайте content непосредственно перед записью и не держите документ в памяти между прогонами.

Тип страницы при этом менять нельзя: markdown-строка в блочной странице покажется одним абзацем, а JSON-документ в markdown-странице — простынёй фигурных скобок. Если формат не тот, заведите новую страницу.

Всё, что выводится из содержимого, сервер пересобирает при каждой записи, отдельных запросов для этого не нужно: pageLink-метки и ссылки вида /storages/<id>/pages/<id> превращаются в список связанных страниц, а mention-метки — в уведомления упомянутым и в строки ленты проекта.

Рецепты

Создать задачу в конкретной колонке

Достаточно указать statusId — доска и проект определяются автоматически:

mutation {
  createTask(
    statusId: "42"
    title: "Проверить заявку №1043"
    description: "Пришла из внешней системы"
    priority: "second"
    deadline: "2026-08-15T12:00:00+03:00"
    performerId: "7"
    tagIds: ["3", "5"]
  ) {
    task { id title status { title } performer { fullName } }
  }
}

Создать задачу во «входящих» пространства

mutation {
  createTask(title: "Разобрать позже") {
    task { id }
  }
}

Найти нужную колонку по названию

Идентификаторы статусов удобно один раз получить и закешировать у себя — они не меняются:

query {
  boards(projectId: "12") {
    items {
      id
      title
      statuses { id title icon }
    }
  }
}

Передвинуть задачу и закрыть её

mutation Advance($id: ID!, $statusId: ID!) {
  moveTask(id: $id, statusId: $statusId, position: 1) {
    task { id status { title } }
  }
}

mutation Finish($id: ID!) {
  completeTask(id: $id) {
    rescheduled           # true — задача повторяющаяся и перенесена на след. итерацию
    task { isCompleted completedAt }
  }
}
Если для перехода между статусами настроена автоматическая смена исполнителя, moveTask применит её — и отправит исполнителю уведомление, как при перетаскивании карточки мышью.

Очистить необязательное поле

Мутации изменения затрагивают только те поля, которые вы передали. Чтобы очистить поле, передайте null явно:

mutation {
  clearDeadline: updateTask(id: "301", deadline: null) { task { deadline } }
  unassign:      updateTask(id: "302", performerId: null) { task { performer { id } } }
}

Записать значения пользовательских полей

query FieldsOfProject {
  taskFields(projectId: "12") {
    items { id title fieldType options builtinKey }
  }
}

mutation {
  setTaskFieldValues(
    taskId: "301"
    values: [
      { fieldId: "8",  value: "ООО «Ромашка»" },        # text
      { fieldId: "9",  value: 14990 },                  # number
      { fieldId: "10", value: "opt_a1b2c3" },           # select — ОДИН id опции
      { fieldId: "11", value: ["opt_x1", "opt_y2"] },   # multiselect — список id
      { fieldId: "12", value: null }                    # очистить значение
    ]
  ) {
    task { fieldValues { fieldId value } }
  }
}
Формат значения зависит от типа поля: select принимает один идентификатор опции строкой, multiselectсписок идентификаторов, checkbox — булево, date и datetime — строку ISO-8601. Идентификаторы опций берите из поля options запроса taskFields: они стабильны и не меняются при переименовании опции.

Пустое значение любого типа (null, пустая строка, пустой список) трактуется как «очистить поле»: значение удаляется, а не сохраняется пустым.

Задачи с дедлайном на неделю вперёд

query Upcoming($from: DateTimeScalar!, $to: DateTimeScalar!) {
  tasks(
    filter: { isCompleted: false, deadlineFrom: $from, deadlineTo: $to }
    orderBy: DEADLINE
    orderDirection: ASC
    limit: 200
  ) {
    totalCount
    items {
      id title deadline
      project { title }
      performer { fullName email }
    }
  }
}

Завести страницу в базе знаний

mutation {
  createPage(
    storageId: "4"
    title: "Регламент обработки заявок"
    pageType: "markdown"
    content: "## Шаг 1\n\nПроверить реквизиты."
  ) {
    page { id title pageType }
  }
}

Так создаётся простая markdown-страница. Для полноценной статьи — с таблицами, диаграммами, колонками, чек-листами и карточками задач — нужен pageType: "blocks" и JSON-документ в content: формат описан в разделе Блочный редактор. Тип страницы фиксируется при создании и потом не меняется.

Инкрементальная синхронизация

Чтобы не выкачивать всё пространство при каждом прогоне, используйте фильтр updatedFrom. Он есть у задач, проектов и страниц и отдаёт всё, что изменилось после указанного момента.

query Changed($since: DateTimeScalar!, $offset: Int!) {
  tasks(
    filter: { updatedFrom: $since }
    orderBy: UPDATED
    orderDirection: ASC
    limit: 200
    offset: $offset
  ) {
    items { id title updatedAt isCompleted status { id } }
    hasMore
    limit
  }
}
Сохраняйте отметку времени начала прогона, а не его конца, и берите её с небольшим перекрытием (например, минус минута). Иначе объекты, изменённые в момент между первой и последней страницей, могут не попасть ни в этот прогон, ни в следующий.

Удаления через updatedFrom не приходят: удалённый объект просто перестаёт появляться в выдаче. Если для вас важны удаления, сверяйте набор идентификаторов со своей копией.

Справочник: запросы

Список собран прямо из работающей схемы, поэтому не может устареть. Полное описание типов — в SDL-схеме; её же удобно скормить генератору типизированных клиентов.

Полный список запросов с аргументами и возвращаемыми типами доступен в SDL-схеме и подгружается в интерактивном виде после загрузки страницы.

Справочник: мутации

Полный список мутаций с аргументами и возвращаемыми типами доступен в SDL-схеме и подгружается в интерактивном виде после загрузки страницы.

Ломающие изменения контракта выпускаются только с повышением версии в адресе эндпоинта. Новые необязательные поля и аргументы могут добавляться в текущую версию — пишите клиентов так, чтобы неизвестные поля в ответе их не ломали.