CI HUBCI HUB SDK
Building an Adapter

Tasks

Task handlers connect CI HUB's tasking UI to work management platforms. Users can search tasks, view task details, attach/remove assets, add comments, and update task states — all from inside the creative tool.

Enabling Tasks

Set tasking: true and configure taskSearch in capabilities:

capabilities: {
  tasking: true,
  taskSearch: {
    help: {
      en: 'Search for tasks by typing a name',
      de: 'Suchen Sie nach Aufgaben indem Sie einen Namen eintippen'
    }
  }
}

searchTasks

Returns a filtered, paginated list of tasks. CI HUB calls this when the user types in the task search bar or applies filters.

Inputs

FieldDescription
requestData.query.querySearch text
requestData.query.filtersApplied filter option IDs (array of "filterName:optionId" strings)
requestData.query.morePagination cursor from previous response
requestData.query.sizePage size

Response

return send({
  tasks: [...],              // array of task objects
  filters: [...],            // filter definitions with selected state
  more: 'cursor-or-offset',  // pagination cursor, undefined if no more
  totalTasksCount: 0         // total count (if known)
})

Patterns

Example - Parses filters, supports workspace scoping and completion status:

const performTaskSearch = async (locals, requestData) => {
  const query = requestData.query.query || ''
  const appliedFilters = requestData.query.filters || []
  const more = requestData.query.more || undefined
  const size = Math.min(Number.parseInt(requestData.query.size || 50, 10), config.get('adapter.maxQuerySize'))
  const parsedFilters = appliedFilters.reduce((acc, optionId) => {
    const [ filterName, optionName ] = optionId.split(':')

    acc[filterName] = optionName

    return acc
  }, {})
  const result = { tasks: [], filters: [ getGlobalTaskListFilter(parsedFilters), getCompletedStatusFilter(parsedFilters) ], more: undefined, totalTasksCount: 0 }
  const workspaceId = parsedFilters.workspace || await getDefaultWorkspaceId(locals)
  const completed = [ undefined, 'completedAndIncomplete' ].includes(parsedFilters.completedStatusFilter) ? undefined : parsedFilters.completedStatusFilter === 'completed'
  const userID = parsedFilters.globalTaskListFilter === 'myTasks' ? await getUserId(locals) : undefined
  const data = (await callApi(locals, 'GET', `/workspaces/${workspaceId}/tasks/search`, {
    'created_at.before': more,
    'limit': size,
    'text': query,

    'opt_fields': optFieldsTask,

    'sort_by': 'created_at',
    'assignee.any': userID,
    completed
  })).data || []

  result.tasks = await extractTasksData(locals, data)

  if (result.tasks.length && result.tasks.length === size) {
    result.more = new Date(result.tasks[result.tasks.length - 1].created || 0).toISOString()
  }

  return result
}

searchTasks: async (locals, requestData) => {
  try {
    const result = await performTaskSearch(locals, requestData)

    return send(result)
  } catch (err) {
    return sendError(err, 400)
  }
},

The handler delegates to performTaskSearch which is reused by both searchTasks and the search handler (when tasking is enabled in folder navigation).

Search Tasks Schema


getTask

Returns full details for a single task.

Inputs

FieldDescription
requestData.params.taskIdTask ID

Response

Return a single task object (same shape as items in searchTasks).

Patterns

Example - Returns full details for a single task:

getTask: async (locals, requestData) => {
  try {
    const { taskId } = requestData.params

    const data = (await callApi(locals, 'GET', `/tasks/${taskId}`)).data
    const task = (await extractTasksData(locals, [ data ]))[0]

    if (!task) {
      return sendError('Error processing task', 400)
    }

    return send(task)
  } catch (err) {
    return sendError(err, 400)
  }
},

Get Task Schema


getTaskAssets

Returns the assets (attachments) linked to a task.

Inputs

FieldDescription
requestData.params.taskIdTask ID
requestData.query.sizePage size
requestData.query.morePagination cursor

Response

return send({
  assets: [...],
  more: 'next-offset',
  totalAssetsCount: 0
})

Patterns

Example - Returns the assets linked to a task:

getTaskAssets: async (locals, requestData) => {
  try {
    const { taskId } = requestData.params
    const limit = Math.min(Number.parseInt(requestData.query.size || 50, 10), config.get('adapter.maxQuerySize'))

    const result = {
      assets: [],
      more: undefined,
      totalAssetsCount: 0
    }

    const taskAssets = await callApi(locals, 'GET', `/tasks/${taskId}/attachments`, {
      opt_fields: optFieldsAttachment,
      offset: (requestData.query.more !== '' && requestData.query.more) || undefined,
      limit
    }) || {}

    if (taskAssets.data && taskAssets.data.length) {
      result.assets = extractAssetData(locals, taskAssets.data).map(asset => ({
        ...asset,
        capabilities: {
          ...asset.capabilities,
          canDeleteAsset: true
        }
      }))
    }

    if (taskAssets.next_page) {
      result.more = taskAssets.next_page.offset
    }

    return send(result)
  } catch (err) {
    return sendError(err, 400)
  }
},

Set canDeleteAsset: true on task assets to enable the remove button in the task assets panel.

Get Task Assets Schema


addTaskComment

Posts a text comment to a task.

Inputs

FieldDescription
requestData.params.taskIdTask ID
requestData.body.commentComment text

Response

return sendStatus(200)

Patterns

Example - Adds a comment to a task:

addTaskComment: async (locals, requestData) => {
  try {
    const { taskId } = requestData.params
    const { comment } = requestData.body

    const data = {
      data: {
        is_pinned: false,
        text: comment
      }
    }

    await callApi(locals, 'POST', `/tasks/${taskId}/stories`, null, data)

    return sendStatus(200)
  } catch (err) {
    return sendError(err, 400)
  }
},

Add Task Comment Schema


addTaskAsset

Attaches an asset to a task. The asset is linked as an external reference using CI HUB's view URL system.

Inputs

FieldDescription
requestData.params.taskIdTask ID
requestData.body.nameAsset display name
requestData.body.providerIdSource adapter provider ID
requestData.body.remoteSystemPrefixSource adapter system prefix
requestData.body.assetIdAsset ID in the source system
requestData.body.viewUrlBase view URL for the asset

Response

return sendStatus(200)

Patterns

Example - Attaches as an external link using multipart form data:

addTaskAsset: async (locals, requestData) => {
  try {
    const { taskId } = requestData.params
    const { name, providerId, remoteSystemPrefix: systemPrefix, assetId, viewUrl } = requestData.body

    if ([ taskId, name, providerId, systemPrefix, assetId, viewUrl ].every(val => ![ undefined, null ].includes(val))) {
      const assetViewUrl = createAssetViewUrl(viewUrl, providerId, systemPrefix, assetId)
      const fd = new FormDataNode()

      fd.append('parent', taskId)
      fd.append('name', name)
      fd.append('resource_subtype', 'external')
      fd.append('url', assetViewUrl)
      await callApi(locals, 'POST', '/attachments', null, fd, fd.getHeaders())

      return sendStatus(200)
    }

    return sendError('Not implemented', 400)
  } catch (err) {
    return sendError(err, 400)
  }
},

createAssetViewUrl() generates a CI HUB view URL that links back to the asset in its source system. Use it to create cross-adapter asset references.

Add Task Asset Schema


deleteTaskAsset

Removes an asset attachment from a task.

Inputs

FieldDescription
requestData.params.taskIdTask ID
requestData.params.assetIdAttachment ID to remove

Response

return sendStatus(200)

Patterns

Example - Removes an asset attachment from a task:

deleteTaskAsset: async (locals, requestData) => {
  try {
    const { assetId } = requestData.params

    await callApi(locals, 'DELETE', `/attachments/${assetId}`)

    return sendStatus(200)
  } catch (err) {
    return sendError(err, 400)
  }
},

Delete Task Asset Schema


updateCustomTaskState

Updates a task's status or custom field value. CI HUB calls this when the user changes a state dropdown in the task detail panel.

Inputs

FieldDescription
requestData.params.taskIdTask ID
requestData.query.stateIdState field identifier
requestData.query.selectedOptionIdNew value/option ID

Response

return sendStatus(200)

Patterns

Example - Supports both built-in fields and custom fields (prefixed with custom_userField_id:):

updateCustomTaskState: async (locals, requestData) => {
  try {
    const { taskId } = requestData.params
    const { stateId, selectedOptionId } = requestData.query
    let data

    if (stateId.startsWith('custom_userField_id:')) {
      const [ , fieldID ] = stateId.split(':')

      data = {
        data: {
          custom_fields: { [fieldID]: selectedOptionId }
        }
      }
    } else {
      data = {
        data: {
          [stateId]: selectedOptionId
        }
      }
    }

    await callApi(locals, 'PUT', `/tasks/${taskId}`, null, data)

    return sendStatus(200)
  } catch (err) {
    return sendError(err, 400)
  }
},

Example - Simpler API, maps directly to a status field:

updateCustomTaskState: async (locals, requestData) => {
  try {
    const { taskId } = requestData.params
    const { selectedOptionId } = requestData.query

    await callApi(locals, 'PUT', '/task', { ID: taskId, updates: JSON.stringify({ status: selectedOptionId }) })

    return sendStatus(200)
  } catch (err) {
    return sendError(err, 400)
  }
},

The stateId and selectedOptionId values come from the task's states array, which is populated in extractTasksData. Each state has options with selectable values.

Update Custom Task State Schema

On this page