Canvas Kit Custom App: Capturing Level of Effort Without Leaving Intercom
Support leads wanted a number that did not exist anywhere: minutes of effort per case. Intercom's Inbox has no field for it, and Canvas Kit, the framework for adding one, cannot store data locally. So the field had to be built, hosted outside the platform, and made to write somewhere else entirely. We designed a Canvas Kit app that renders inside the Inbox, holds no data of its own, and pushes Level of Effort straight to the Salesforce case.
Executive Summary
Context
A UI framework with no persistence layer changes what a custom field means. The app cannot remember what an agent typed, so every submission has to resolve its own destination at the moment it is submitted. That turns a simple input box into a question about identity: which case does this belong to, and what happens if that case does not exist yet.
What We Built
A specified Canvas Kit app injected into the Intercom Inbox, hosted externally and using initialize and submit webhook flows. On submit, it pushes the Level of Effort value one way to Salesforce, matched to the correct Case by Conversation ID, and creates the case if the scheduled sync has not produced one yet, so agents can do this without leaving Intercom.
Tech Stack
- Intercom Canvas Kit
- initialize/submit webhooks
- Node.js
- Salesforce Service Cloud
- Case object
- custom Task
- Connected App
Platforms where you can add a custom field natively. This is a design for the case where the UI framework offers no persistence and the value has to land in another system.
The Challenge
Support leads needed Level of Effort recorded per case, in minutes, by the agent who did the work. Intercom's Inbox had no native field for it, so the only route was a custom app. Canvas Kit offers no local data persistence. A Canvas Kit app renders, accepts input, and forgets. Anything an agent types has to leave the platform in the same request or it is gone, which rules out the usual pattern of collecting a value and syncing it later. Agents were the constraint on the interface. Any design that sent them to Salesforce to log minutes would not be used, so the input had to appear inside the Inbox they already worked in. The destination case might not exist yet. Effort gets logged during a live conversation, and the scheduled sync runs on its own 30-minute cycle, so the app can be asked to write to a case that has not been created.
Our Approach
The app was designed stateless on purpose, because the framework left no alternative. It uses the initialize webhook to render inside the Inbox and the submit webhook to act. There is no intermediate store, so a submitted value either reaches Salesforce or fails visibly. On submit, Level of Effort is pushed one way to Salesforce. We considered holding it in Intercom and syncing it later, and we rejected that: Intercom has nowhere to hold it. A second copy would also introduce a reconciliation problem between two systems that already disagree about what a case is. Conversation ID matches the submission to its case, the same key the scheduled integration uses. When no case exists yet, the app creates one rather than failing or queuing, so a live agent is never blocked by a batch that has not run. Each submission becomes a Case-linked Task with its type stamped and the recorded time attached, which keeps effort reportable against the case without widening the Case object itself. One requirement stayed open. Auto-closing a case on session timeout or agent logout has no native Intercom event behind it, and the design records it as needing further investigation rather than claiming a solution.
This engagement is part of our customization + optimization work for technology & software organisations.
Impact
Effort captured where the work happens
The design has agents record minutes in the Intercom Inbox they already have open, with no second system to switch to.
A reportable number that did not exist
The design writes Level of Effort as a Case-linked Task in Salesforce with time attached, so support effort can be reported per case.
No dependency on the sync schedule
The design has the app create the case when one is missing, so a live agent can log effort during a conversation the 30-minute batch has not reached yet.
Effort stored without widening the Case object
Each submission becomes a Case-linked Task with its type stamped and the recorded time attached, which keeps effort reportable against the case without adding fields to the Case object.
Canvas Kit cannot persist, so there is no collect-then-sync option. The submit handler has to be the whole transaction, which simplifies the design and removes an entire class of drift between two copies of the same value.
The value only exists if a human enters it, so the interface decision is the data-quality decision. Injecting the input into the Inbox is what makes the metric usable.
The custom app and the scheduled pipeline both resolve a case by Conversation ID. One key across two independent paths is why they can write to the same record without coordinating.
When the case may not exist yet, creating it on submit beats retrying later. The agent's action completes, and the batch finds the same record on its next pass.
Canvas Kit keeps no local data, so a Level of Effort value is lost unless it leaves in the same request. The case in Salesforce might not exist yet either, because the sync runs every 30 minutes. Now the submit webhook pushes the value one way to Salesforce and matches it to the Case by Conversation ID. If there's no case yet, the app creates one.
FAQ
The app forwards the value instead of storing it. Intercom's Canvas Kit renders a UI and handles a submit webhook, but it has nowhere to keep what the user typed. So the app treats submission as the entire transaction: the value goes straight to the system of record, in this case a Salesforce Case-linked Task, in the same request. Nothing sits in the middle waiting to be synced, which also means nothing can silently fall out of sync.
The app creates the case. Effort gets logged live, while the scheduled integration runs on its own 30-minute cycle, so the two are guaranteed to disagree sometimes. Rather than queue the submission or show the agent an error, the app resolves the case by Conversation ID and creates one when there is no match. The batch sync then finds that same case on its next pass, because both paths use the same key.
Continue reading