Skip to main content

Send actions to the Action Hub

The Action Hub in the Sales Hub lists the actions that sales reps work on: a quote to follow up, a Contact to call, a Company that stopped ordering. Workflows and integrations create these actions through the API. The rep sees each action with a button that opens the right page, picks it up and completes it.

In the API an action is a ticket. Create one with the ticketCreate mutation of the GraphQL API or with POST /v2/admin/tickets in the REST API. Creating an action needs an API key with the user.OWNER role.

Create an action​

This action asks a sales rep to follow up on quote 1752. The rep gets a View Order button that opens the quote.

mutation CreateAction($input: TicketCreateInput!) {
ticketCreate(input: $input) {
id
type
status
orderId
assignedToAdminUser {
firstName
lastName
}
createdAt
}
}

Variables​

{
"input": {
"titles": [
{ "language": "EN", "value": "Follow up on quote 1752" },
{ "language": "NL", "value": "Offerte 1752 opvolgen" }
],
"descriptions": [
{ "language": "EN", "value": "The quote has been open for two weeks. Ask the customer whether they want to place the order or need changes." },
{ "language": "NL", "value": "De offerte staat al twee weken open. Vraag de klant of hij wil bestellen of iets wil aanpassen." }
],
"type": "VIEW_ORDER",
"orderId": 1752,
"status": "OPEN",
"assignedToAdminUserId": 65,
"sources": [{ "name": "Quote follow-up", "id": "quote-follow-up-1752" }]
}
}

Response​

{
"data": {
"ticketCreate": {
"id": "01a139c5-8071-7456-bdab-dd180604786b",
"type": "VIEW_ORDER",
"status": "OPEN",
"orderId": 1752,
"assignedToAdminUser": {
"firstName": "Jasper",
"lastName": "Pardijs"
},
"createdAt": "2026-10-14T09:36:47.219Z"
}
}
}

titles and status are required. Create new actions as OPEN.

The sales rep with admin user ID 65 now finds the action at the top of My Actions in the Action Hub, with the title in their own language, the description and their name. The bell in the Sales Hub shows the new action within about a minute, and the Action Hub counts it as new for an hour. View Order opens the quote in a new tab.

Choose the button​

The type decides which button the action gets, and each type needs one field that the button uses:

typeNeedsButtonThe button opens
VIEW_ORDERorderIdView OrderThe order, quote or request, in a new tab
VIEW_COMPANYcompanyIdView CompanyThe page of the Company
VIEW_CONTACTcontactIdView ContactThe page of the Contact
VIEW_CUSTOMERcustomerIdView CustomerThe page of the Customer
VIEW_PRODUCTproductIdView ProductThe product in the Backoffice
VIEW_CLUSTERclusterIdView ClusterThe cluster in the Backoffice
SEND_EMAILemailSend EmailA new email to that address, in the rep's email program
PHONE_CONTACTphoneCallThe phone app of the rep's device, with that number
VISIT_EXTERNAL_URLexternalUrlOpen LinkThe URL, in a new tab
NOTIFICATIONNothingNoneNothing. The action only informs, and the rep archives it
  • Write the type exactly as in the table. Types are in capitals with underscores. A type such as view_order is rejected with "Name must be in SCREAMING_SNAKE_CASE format (all caps divided by underscores)".
  • Fill in the field the button needs. Without it the API still creates the action, but it has no button.
  • Another type gets no button. The API accepts any type in capitals, for example CHURN_RISK. The action then has no button, and the rep can only pick it up, complete it or archive it.
  • Check the ID yourself. The API does not check that the linked record exists, so an action for a deleted order still gets a View Order button. The rep gets an error when they click it.
  • Link more records when they help. The button uses only its own field, but you can store the Company, Contact or order on any action, for example to find it again with a search.

The product and cluster pages are in the Backoffice, so the rep needs Backoffice access and the Product Data role to open them.

The examples below use the mutation from Create an action. Only the variables change.

Ask a rep to call a Contact​

{
"input": {
"titles": [{ "language": "EN", "value": "Call Lotte Jansen about her delivery" }],
"descriptions": [{ "language": "EN", "value": "Lotte asked to be called back about the delivery date of her last order." }],
"type": "PHONE_CONTACT",
"phone": "+31201234502",
"contactId": 93,
"status": "OPEN",
"assignedToAdminUserId": 65,
"sources": [{ "name": "Callback request", "id": "callback-93" }]
}
}

The action gets a Call button. Use SEND_EMAIL with email the same way for a Send Email button.

Open a page outside Propeller​

VISIT_EXTERNAL_URL opens any URL, for example the account in your CRM. buttonLabels replaces the default Open Link with your own text:

{
"input": {
"titles": [{ "language": "EN", "value": "Update the NovaSensa IoT B.V. account in the CRM" }],
"descriptions": [{ "language": "EN", "value": "The company changed its invoice address. Update it in the CRM as well." }],
"buttonLabels": [
{ "language": "EN", "value": "Open in CRM" },
{ "language": "NL", "value": "Openen in CRM" }
],
"type": "VISIT_EXTERNAL_URL",
"externalUrl": "https://crm.example.com/accounts/novasensa",
"companyId": 36,
"status": "OPEN",
"assignedToAdminUserId": 65,
"sources": [{ "name": "CRM sync", "id": "crm-sync-36" }]
}
}

Inform without a button​

A NOTIFICATION tells the rep something that needs no work. It has no button and no menu, only Archive:

{
"input": {
"titles": [{ "language": "EN", "value": "Price sheet import finished" }],
"descriptions": [{ "language": "EN", "value": "The nightly import updated 1,240 prices. No action needed." }],
"type": "NOTIFICATION",
"status": "OPEN",
"assignedToAdminUserId": 65,
"sources": [{ "name": "Price import", "id": "price-import-20261014" }]
}
}

Write the text​

titles, descriptions and buttonLabels are lists of localized values. The Action Hub shows the value in the language the rep has selected in the Sales Hub. When there is none, it shows the English value, and otherwise the first one. Always include an English value.

The Action Hub shows the first 200 characters of the description, with Show more for the rest. Put what the rep has to do in that first part.

Without buttonLabels, the button has the English label from the table in Choose the button.

Assign a sales rep​

assignedToAdminUserId assigns the action to a sales rep. It takes the admin user ID of the rep, the user who signs in to the Sales Hub. The adminUsers query lists admin users with their IDs. It needs an API key with the role.OWNER, role.EDITOR or role.VIEWER role.

By default the Action Hub shows My Actions: the open and in progress actions assigned to the rep who is signed in. An assigned action:

  • appears in My Actions of that rep, with the rep's name under it
  • shows in the bell of that rep

An action without assignedToAdminUserId belongs to the whole team. It appears only under All Actions, where any rep can pick it up, and the bell does not show it. This action asks the team to contact a Company that stopped ordering:

{
"input": {
"titles": [{ "language": "EN", "value": "MeterTech Solutions GmbH has not ordered in 90 days" }],
"descriptions": [{ "language": "EN", "value": "The company used to order twice a month. Check in before they move to another supplier." }],
"type": "VIEW_COMPANY",
"companyId": 37,
"status": "OPEN",
"sources": [{ "name": "Churn monitor", "id": "churn-37" }]
}
}

Name the source​

sources records where the action comes from. On an action without a rep, the Action Hub shows the name of the first source where the rep's name would be, for example "Churn monitor". Use the id for your own reference, such as the run or the record that triggered the action.

Avoid duplicate actions​

A workflow that runs every day can find the same Company again tomorrow. Before you create an action, search for an open one for the same record and type, and create the action only when the search finds nothing:

query OpenActionsForCompany($input: TicketSearchInput) {
tickets(input: $input) {
itemsFound
items {
id
type
status
createdAt
}
}
}
{
"input": {
"types": ["VIEW_COMPANY"],
"companyIds": [37],
"statuses": ["OPEN", "IN_PROGRESS"],
"page": 1,
"offset": 10
}
}
{
"data": {
"tickets": {
"itemsFound": 1,
"items": [
{
"id": "01a139c5-8548-723b-a12e-cd0b966949b9",
"type": "VIEW_COMPANY",
"status": "OPEN",
"createdAt": "2026-10-14T09:36:48.457Z"
}
]
}
}
}

The search also filters on orderIds, contactIds, customerIds, productIds, clusterIds, emails, phones, externalUrls and assignedToAdminUserIds.

Follow the status​

StatusMeaning
OPENNobody works on it yet
IN_PROGRESSA rep picked it up. Pick Up sets this status, assigns the action to that rep and fills pickedUpAt
COMPLETEDThe work is done. Setting this status fills completedAt
ARCHIVEDCleared away without work. Unarchive makes the action OPEN again

The Action Hub shows open and in progress actions unless the rep changes the filter, so a completed or archived action leaves the list.

When the reason for an action goes away, for example because the quote became an order, close the action from your workflow with ticketUpdate:

mutation CompleteAction($id: ID!, $input: TicketUpdateInput!) {
ticketUpdate(id: $id, input: $input) {
id
status
completedAt
}
}
{
"id": "01a139c5-8071-7456-bdab-dd180604786b",
"input": { "status": "COMPLETED" }
}
{
"data": {
"ticketUpdate": {
"id": "01a139c5-8071-7456-bdab-dd180604786b",
"status": "COMPLETED",
"completedAt": "2026-10-14T09:55:08.367Z"
}
}
}

ticketUpdate changes only the fields you send. Set ARCHIVED instead when nobody did the work. ticketDelete removes an action for good.

Use the REST API​

The REST API has the same fields. Create an action with POST /v2/admin/tickets:

curl -X POST https://api.helice.cloud/v2/admin/tickets \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"titles": [{ "language": "EN", "value": "MeterTech Solutions GmbH has not ordered in 90 days" }],
"descriptions": [{ "language": "EN", "value": "The company used to order twice a month. Check in before they move to another supplier." }],
"type": "VIEW_COMPANY",
"companyId": 37,
"status": "OPEN",
"sources": [{ "name": "Churn monitor", "id": "churn-37" }]
}'

The response is the new action with all its fields:

{
"titles": [{ "language": "EN", "value": "MeterTech Solutions GmbH has not ordered in 90 days" }],
"descriptions": [{ "language": "EN", "value": "The company used to order twice a month. Check in before they move to another supplier." }],
"type": "VIEW_COMPANY",
"status": "OPEN",
"companyId": 37,
"sources": [{ "name": "Churn monitor", "id": "churn-37" }],
"id": "01a139c5-ff42-7f27-a0f3-8049a1b833b9",
"buttonLabels": null,
"assignedToAdminUserId": null,
"contactId": null,
"customerId": null,
"productId": null,
"clusterId": null,
"orderId": null,
"email": null,
"phone": null,
"externalUrl": null,
"pickedUpAt": null,
"completedAt": null,
"lastModifiedByAdminUserId": null,
"createdByAdminUserId": null,
"createdAt": "2026-10-14T09:37:19.683Z",
"lastModifiedAt": "2026-10-14T09:37:19.683Z"
}

To avoid duplicates, search with POST /v2/admin/tickets/search and the same filters as in GraphQL:

curl -X POST https://api.helice.cloud/v2/admin/tickets/search \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"types": ["VIEW_COMPANY"],
"companyIds": [37],
"statuses": ["OPEN", "IN_PROGRESS"]
}'

The response holds itemsFound and items, with the same fields as above. Change an action with PATCH /v2/admin/tickets/{id} and remove it with DELETE /v2/admin/tickets/{id}. To get an access token, see Authenticate.

See also​