# Showing the uploader their Box file progress with an @mention comment — start, done, and failure

> A Box folder pipeline comments on each uploaded file at start, completion and failure, mentioning the uploader from the file modified_by field.

- Source: https://oharu121.com/blog/box-mention-comment-file-progress-start-done-failure/
- Published: 2026-11-04T16:05:22+09:00
- Tags: Box, Automation

---
## Introduction

I built a pipeline that starts whenever someone uploads a file into a Box folder, and processes that file. But every time I dropped a file in, I could not tell whether it was being processed. Whether a job succeeded or failed, the uploader got no notification and could only come back later to check whether the file was done.

I wanted the pipeline to report its progress in real time, so the uploader could see how far their file had got. I did not want to build a separate UI, and I did not want to post to Slack either, because the number of notifications grows with the number of users.

The answer was **a comment on the uploaded file that @mentions the uploader**, posted when processing starts, when it finishes with a link to the result, and when it fails for good. Box turns the mention into an email and an inbox notification, so the progress reaches the person waiting on it without them opening Box.

## Which Box signals reach the uploader

I listed the ways the uploader could see progress without building a separate UI:

| Signal | How the uploader finds out | Side effect |
| --- | --- | --- |
| Lock the file | A lock icon next to the file name, no notification | Reads as "you cannot edit this", and a job that crashes leaves the lock behind |
| Write to the description | Shown in the details panel, no notification | Overwrites a field people write in themselves |
| Rename to `[processing] …` | The name changes in the file list, no notification | Fires `FILE.RENAMED`, so the Box webhook that starts the pipeline runs again |
| Box Skills status card | Shown in the preview sidebar, no notification | Only visible with the file preview open |
| Comment with @mention | Shown in the comment panel, with an email and a Box inbox notification | Collaborators with comment emails on also get one |

> **HEADS UP**
>
> **Skills cards show progress but notify nobody**
>
> Box Skills is Box's framework for attaching an external processing service to a folder; the service writes its results back as cards in the global `boxSkillsCards` metadata template. Writing a `status` card directly with `POST /files/{id}/metadata/global/boxSkillsCards` worked without registering a Skill, and the preview sidebar showed it as processing. It notifies nobody, so I left it out.

## A comment on the file, mentioning whoever uploaded it

The pipeline has two components:

1. Box webhook: puts one message on a queue for each upload.
2. Worker: takes messages off the queue, processes the file, and writes the output file back to Box.

The worker reports progress in three comments on the file. It posts them with Box's `POST /comments` endpoint. **The mention goes in `tagged_message`**, the field Box scans for `@[user_id:name]` and turns into a notification.

*Figure — CommentPoints: The worker comments on the original file at three points. The failure comment comes from the last delivery only, after the queue has no retries left.*

1. **Start**, once the worker holds the file's lock, knows the format is supported, and knows this version is not one it already finished.
2. **Done**, after the output file is uploaded, with a link to it.
3. **Failed**, only when the last delivery fails too. The queue delivers a message at most three times, and an earlier failure can still succeed on the next delivery. The worker therefore comments on the file only once the last delivery has also failed.

*Image: Box's Activity panel for one file, in the Japanese UI, holding three comments from the app. Each starts with an @mention of the uploader, then reads "Processing has started. I will comment here again when it finishes.", "Processing finished. Output:" followed by a Box file link, and "Processing failed. The operators have been notified automatically." The comment box is at the bottom*

*The three comments as the uploader sees them in the file's Activity panel. The commenter and mention names are blurred.*

The comment is posted by `post_comment()` in the connector that wraps the Box API, and it is only a few lines. `mention` is required, because a comment with nobody to notify is not a progress report:

```python title="box/client.py"
_MENTION_NAME_DELIMITERS = str.maketrans("", "", "[]:")

def post_comment(self, file_id: str, text: str, *, mention: Person) -> None:
    item = CreateCommentItem(id=file_id, type=CreateCommentItemTypeField.FILE)
    name = mention.name.translate(_MENTION_NAME_DELIMITERS).strip() or mention.id
    tagged = f"@[{mention.id}:{name}] {text}"
    self._call(lambda: self._client.comments.create_comment(None, item, tagged_message=tagged))
```

The display name loses `[`, `]` and `:` because they are the mention syntax's own delimiters, and Box publishes no escape for them. Take a display name of `Sato [QA]: Tokyo`:

```text title="before stripping"
@[12345:Sato [QA]: Tokyo]
```

```text title="after stripping"
@[12345:Sato QA Tokyo]
```

In the string before stripping, the mention syntax ends at the `]` after `QA`. The name only labels the mention, and **Box routes the notification by `user_id`**, so who receives it does not change. The `None` in the last line is Pitfall 1 below.

**A comment that fails to post never fails the job.** An escaping error would send the message back to the queue, and a failed completion comment would reprocess a file whose output was already uploaded. So a failure is only logged as a warning. The warning carries only the exception type, never the uploader's name or the comment text.

> **HEADS UP**
>
> **The comments do not set the webhook off**
>
> The webhook subscribes only to file triggers (`FILE.UPLOADED`, `FILE.MOVED`, `FILE.RENAMED`, `FILE.TRASHED`, `FILE.DELETED`, `FILE.RESTORED`), and posting a comment fires none of them.

## Who to mention: `modified_by`, not `created_by`

The person waiting is **the file's `modified_by`, the user who uploaded its current version**. The worker already calls `GET /files/{id}` for the file name and parent folder, so the uploader costs one more entry in `fields` and no extra request:

```python title="box/client.py" ins={2}
file = self._call(lambda: self._client.files.get_file_by_id(
    file_id, fields=["name", "parent", "size", "version_number", "modified_by"]
))
```

I checked which field follows the uploader by uploading versions from two accounts. With the first version mine, both fields named me. After the app's service account uploaded a new version, `modified_by` named the service account and **`created_by` still named me**.

When I uploaded again, `modified_by` came back to me. `created_by` records whoever put the file there first, so it mentions the wrong person once a colleague replaces the file.

A version the app's own account uploaded needs no comment. So the worker skips the comment when `modified_by` matches the account from `GET /users/me`, which is the app's own account.

> **CAREFUL**
>
> **Looking the user up by id returns 404**
>
> When the app called `GET /users/{id}` for my account, it got `404 Not Found`, so the uploader cannot be looked up separately. `modified_by` already carries the id and the display name the mention needs.

## The mention still arrives with comment emails turned off

A mention reaches the uploader even after they switch off comment emails for the folder. I unchecked "Adds a comment" in the folder's notification settings, which turns off the email for each comment. When the app then posted a mention, **the email and the Box inbox notification both still arrived.**

*Image: A Box notification email in the Japanese UI, under the Box logo, saying the commenter mentioned you in a comment on office-features.docx. A box quotes the comment "Processing has started. I will comment here again when it finishes.", and a blue reply button sits below*

*The email Box sends for the mention, quoting the comment and linking the file.*

*Image: The Box inbox panel open under the inbox tray icon, in the Japanese UI, with an unread card saying the commenter mentioned you on office-features.docx, quoting "Processing failed. The operators have been notified automatically."*

*Mentions also arrive in the Box inbox, marked unread; this one is from the failure comment.*

So the comments do not have to flood a shared folder. A collaborator who does not want the progress of other people's files turns off comment emails on that folder, and still hears about their own uploads through the mention.

## Pitfall 1: Box returns 400 when `message` and `tagged_message` are both sent

`create_comment()` in the Box Python SDK sends `message` and `tagged_message` together, and **Box rejects that pair**. The SDK, `box_sdk_gen`, makes `message` a required positional argument and builds the request body with both keys:

```python title="box_sdk_gen/managers/comments.py"
request_body: Dict = {
    'message': message,
    'tagged_message': tagged_message,
    'item': item,
}
```

Passing the comment text as `message` and the same text with the mention added as `tagged_message` reads like what the signature asks for. But against real Box, every call came back:

```text
Message: 400 Bad Request; Request ID: …
	Status code: 400
	Code: bad_request
```

[Box's own guide](https://developer.box.com/guides/comments/create-comment) already says to pass a string with a mention "as the `tagged_message` instead of the `message`". The SDK still requires `message`, so I passed `None`. The SDK's `serialize()` drops keys whose value is `None`, so **the request body that goes out carries `tagged_message` alone**:

```python title="message as text (Box returns 400)"
# SDK arguments
create_comment(text, item, tagged_message=tagged)

# Request body sent
{
  "message": "Processing has started.",
  "tagged_message": "@[12345:Sato] Processing has started.",
  "item": { "id": "…", "type": "file" }
}
```

```python title="message as None (comment created)"
# SDK arguments
create_comment(None, item, tagged_message=tagged)

# Request body sent
{
  "tagged_message": "@[12345:Sato] Processing has started.",
  "item": { "id": "…", "type": "file" }
}
```

## Pitfall 2: announcing the start only on the first delivery skips it after lock contention

Gating the start comment on the first delivery drops it whenever that delivery cannot take the file's lock. The worker holds a lock per file in a DynamoDB job record, so two jobs never process the same file at once, and a job that finds the lock taken goes back to the queue for a later delivery.

Box can send `FILE.MOVED` and `FILE.UPLOADED` for the same file at the same moment, so losing the lock is an ordinary path. The queue counts deliveries in `ApproximateReceiveCount`, and a gate that announces only when the count is 1 fails on it.

Delivery 1 loses the lock and announces nothing. Delivery 2 takes the lock and processes the file, but its count is 2, so it announces nothing either. **The uploader gets the completion comment with no start before it.**

*Figure — DeliveryTimeline: Delivery 1 is refused the lock, and delivery 2 does the work. A count-based gate stays silent on both; the version marker announces on delivery 2.*

A rerun breaks the same gate. When a new version arrives while the worker is busy, the worker processes again inside the same delivery, and that newer version is never announced if the count is already 2 or more.

The fix is to **record which version of the file was announced, on the DynamoDB record that already holds the file's lock**, in a field named `start_notified`.

The worker writes that field with a conditional update, which succeeds only when `start_notified` is not already the current version. So only one write per version succeeds, and the start comment goes out once:

```python title="state.py"
def claim_start_notice(self, key: str, *, source_version: str | None) -> bool:
    try:
        self._table.update_item(
            Key={"pk": key},
            UpdateExpression="SET start_notified = :ver",
            ConditionExpression=(
                "attribute_not_exists(start_notified) OR start_notified <> :ver"
            ),
            ExpressionAttributeValues={":ver": source_version or ""},
        )
    except ClientError as exc:
        if exc.response["Error"]["Code"] != "ConditionalCheckFailedException":
            raise
        return False
    return True
```

*Figure — StartNoticeClaim: The conditional update decides whether a delivery posts the start comment. Completion clears start_notified, and a failure keeps it.*

Completing a job removes `start_notified`, and a failure leaves it in place, so:

- **Lock refused on delivery 1.** `start_notified` was never written, so delivery 2 announces the start.
- **Retry after a failure.** `start_notified` is already this version, so the retry stays silent.
- **Rerun for a newer version.** Completion cleared `start_notified`, so the rerun announces.
- **New version after a final failure.** `start_notified` still holds the old version, so the new one is announced.

## Summary

Box now tells the uploader that processing started, that it finished with a link to the result, or that it failed. The file's comment panel keeps that history. I relied entirely on Box's own mechanisms: the worker now posts three kinds of comment, one per processing state, and @mentions the uploader in each.

What the work taught me was mostly about Box. A mention is delivered separately from comment emails, so collaborators can mute a busy folder without missing their own files. `modified_by` names the uploader of the current version, where `created_by` keeps naming the first one.

Passing `None` as `message` to the SDK's `create_comment()` sends `tagged_message` alone, which Box accepts. Recording the start per file version in `start_notified` announces each version once, whichever delivery gets the lock.

## References

- [Box API reference: Create comment](https://developer.box.com/reference/post-comments)
- [Box developer guide: Create comment, including the `@[user_id:name]` mention syntax](https://developer.box.com/guides/comments/create-comment)
- [Box support: Box Folder Settings, including the folder email notification options](https://support.box.com/hc/en-us/articles/360043696014-Box-Folder-Settings)
- [Box API reference: Status Skill card](https://developer.box.com/reference/resources/status-skill-card/)
- [Box developer guide: V2 webhooks and their triggers](https://developer.box.com/guides/webhooks/v2)
