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
| Field | Description |
|---|---|
requestData.query.query | Search text |
requestData.query.filters | Applied filter option IDs (array of "filterName:optionId" strings) |
requestData.query.more | Pagination cursor from previous response |
requestData.query.size | Page 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).
getTask
Returns full details for a single task.
Inputs
| Field | Description |
|---|---|
requestData.params.taskId | Task 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)
}
},getTaskAssets
Returns the assets (attachments) linked to a task.
Inputs
| Field | Description |
|---|---|
requestData.params.taskId | Task ID |
requestData.query.size | Page size |
requestData.query.more | Pagination 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.
addTaskComment
Posts a text comment to a task.
Inputs
| Field | Description |
|---|---|
requestData.params.taskId | Task ID |
requestData.body.comment | Comment 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)
}
},addTaskAsset
Attaches an asset to a task. The asset is linked as an external reference using CI HUB's view URL system.
Inputs
| Field | Description |
|---|---|
requestData.params.taskId | Task ID |
requestData.body.name | Asset display name |
requestData.body.providerId | Source adapter provider ID |
requestData.body.remoteSystemPrefix | Source adapter system prefix |
requestData.body.assetId | Asset ID in the source system |
requestData.body.viewUrl | Base 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.
deleteTaskAsset
Removes an asset attachment from a task.
Inputs
| Field | Description |
|---|---|
requestData.params.taskId | Task ID |
requestData.params.assetId | Attachment 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)
}
},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
| Field | Description |
|---|---|
requestData.params.taskId | Task ID |
requestData.query.stateId | State field identifier |
requestData.query.selectedOptionId | New 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.