A Comment Thread in a Canvas App, Done Properly
The naive implementation works with ten comments and breaks with a thousand. The difference is where the filtering happens.
Rajiv KumarJanuary 3, 20245 min readAdding comments to a record in a canvas app is a common requirement and a good illustration of delegation, concurrency and refresh. The straightforward version is subtly wrong in three ways, all of which appear only once real data arrives.
Comments attached to a record are one of the most requested additions to an internal application: a request, a case or an approval, with a running thread of who said what. It is a small feature and a good illustration of three canvas-app failure modes, because the obvious implementation trips all three.
The data model
Comments belong in their own table, with a column holding the parent record's identifier. Not in a multi-line text column on the parent.
The text-column version is tempting because it is one less table. It fails as soon as two people comment at once — each is editing the same column, and the second write overwrites the first with no error and no trace. It also makes "who said this, and when" unanswerable, and cannot be filtered or reported on.
Minimum useful shape:
ParentId— the record this comment belongs to. Index it.CommentText— the body.AuthorandCreated— usually supplied by the platform.Status— optional, if comments can be resolved or withdrawn.
Failure one: filtering in the wrong place
The version everyone writes first:
// WRONG. This is not delegable against most sources.
Filter(
Comments,
Text(ParentId) = Text(varRecord.ID)
)
Wrapping a column in Text() defeats delegation. The source cannot evaluate the expression, so Power Apps retrieves records up to the row limit and filters them locally. With fifty comments in the table it works perfectly. With five thousand, it returns comments from the first page only — and the ones belonging to this record are silently missing.
No error appears. The thread just looks shorter than it is.
The delegable form compares a column to a value of the same type, with no function around the column:
// Delegable: the comparison goes to the source, which returns
// only the rows for this record.
Filter(
Comments,
ParentId = varRecordId
)
Set varRecordId to the correct type when navigating to the screen, rather than converting inside the filter. That single change is the difference between a thread that is correct and one that is correct up to five hundred rows.
Failure two: writing unsafely
The second common shape uses the form control's submit, or patches the whole record. Both are heavier than needed and both can clobber concurrent changes.
Patch with an explicit record creates exactly one row and touches nothing else:
// Creating a new comment. Defaults() makes this a create,
// not an update, and only the named columns are written.
If(
!IsBlank(Trim(txtNewComment.Text)),
Patch(
Comments,
Defaults(Comments),
{
ParentId: varRecordId,
CommentText: Trim(txtNewComment.Text),
Status: "Active"
}
);
Reset(txtNewComment)
)
Two details that matter. Trim plus the blank check stops empty comments, which users will otherwise create by pressing the button twice. And Reset clears the input, without which the next comment starts pre-filled with the previous one — a small thing that produces duplicate comments in practice.
Failure three: refreshing everything
After a write, the gallery needs to show the new comment. The reflex is Refresh(Comments), which re-reads the data source — every record, over the network, on every comment.
A better pattern for the common case is to keep the thread in a collection and add locally:
// Load once for this record, filtered at the source.
ClearCollect(
colComments,
Filter(Comments, ParentId = varRecordId)
);
// After a successful Patch, add the created record to the
// collection rather than re-reading the whole source.
Collect(colComments, patchedRecord)
The trade is explicit: the collection can go stale if someone else comments while this screen is open. For a thread on a record one person is working on, that is usually acceptable, and a manual refresh control covers the rest. For genuinely concurrent editing, re-read on a timer or on focus — but make that a decision rather than the default.
Displaying it well
Sort newest last for a conversation, newest first for a log. Sorting on the created timestamp is delegable on most sources when the column is indexed. Show the author and a relative time, and keep the comment body in a label with auto-height so long comments do not clip — a fixed-height label silently truncating a comment is a bug users will not report but will work around.
Common mistakes
- Comments in a text column on the parent. Concurrent writes overwrite each other silently.
- Wrapping the filtered column in a function. Delegation is lost, and the failure is missing data rather than an error.
- No blank check. Empty comments accumulate.
- No
Resetafter submit. Produces duplicates. - Refreshing the whole source after every write. Slow, and gets slower as the table grows.
- Ignoring the delegation warning. It is telling you the results will be wrong, not that they will be slow.
- No index on the parent column. Even a delegable filter is slow against an unindexed column at scale.
Troubleshooting
| Symptom | Cause |
|---|---|
| Older comments missing from long threads | Non-delegable filter; only the first page was searched |
| Comment saved but not visible | The collection was not updated, or the gallery is bound to the source rather than the collection |
| Duplicate comments | No Reset, or no guard against a second button press |
| Someone else's comment disappears | Comments stored in a single text column on the parent record |
| Thread slow to open | Unindexed parent column, or loading all comments rather than this record's |
| Long comments cut off | Fixed-height label instead of auto-height |
Frequently asked questions
Does this work the same on SharePoint and Dataverse?
The pattern does. What differs is which operations delegate — Dataverse supports more than SharePoint — and how you index the parent column. Verify delegability against the source you are actually using, because the same formula can be delegable on one and not the other.
How do I show the author's name and photo?
Store the author as a person-type column where the source supports it, and read the display name from it. Storing a plain text name means it never updates when someone's name changes.
Can users edit or delete their own comments?
Yes, and enforce it at the data source, not in the UI. Hiding a delete button is not a permission. On Dataverse this is the security model; on SharePoint it needs item-level permissions or an intermediary that performs the check.
Should comments be threaded?
Only if the requirement genuinely needs replies to replies. A flat thread with @-mentions covers most internal use, and threading adds recursion that canvas galleries handle awkwardly.
The general lesson
The comment thread is a small feature that exercises the three things canvas apps get wrong most often: filtering that does not reach the source, writes that are heavier and less safe than they need to be, and refreshing far more than necessary. Get those right here and the same shape works for attachments, history and activity feeds.

