Skip to content

Messaging plugin

dhis2 messaging covers DHIS2's internal messaging surface (/api/messageConversations). Pairs with the files plugin — a MESSAGE_ATTACHMENT-domain fileResource uploaded via dhis2 files resources upload --domain MESSAGE_ATTACHMENT ... can be referenced from a message by UID.

dhis2 messaging {list,get,send,reply,mark-read,mark-unread,delete}

MCP mirrors the full surface: messaging_list, messaging_get, messaging_send, messaging_reply, messaging_mark_read, messaging_mark_unread, messaging_delete.

Scope

  • Private / direct conversations (user ↔ user, user ↔ group, user ↔ orgUnit). The common case.
  • Tickets (feedback, priority, status, assignee) exist as an extra endpoint family on the OpenAPI spec (/assign, /priority, /status). Not wired into this plugin — they're workflow-plugin material. Use post_raw directly if you need them.

Send + reply

# Send a direct message:
dhis2 messaging send "Pilot rollout" "Please review the attached plan." \
  --user YzqyZKXzcxI --user aB3dEf5gH7i

# Attach a previously-uploaded fileResource (send-time only):
dhis2 files resources upload report.pdf --domain MESSAGE_ATTACHMENT  # prints the FR uid
dhis2 messaging send "Report" "latest numbers" \
  --user YzqyZKXzcxI \
  --attachment <fr-uid>

# Reply to the thread:
dhis2 messaging reply <conversation-uid> "thanks — reviewed"

Reply attachment caveat: DHIS2's reply endpoint (POST /api/messageConversations/{uid}) takes a text/plain body on v42 — it stores whatever bytes arrive as the message text. Attachments + the internal-note flag only work on the initial send call. To attach a second file after a thread exists, start a new conversation referencing the earlier one in the subject.

List + inbox filtering

# Full inbox (rich table):
dhis2 messaging list

# Unread only (DHIS2 filter syntax):
dhis2 messaging list --filter "read:eq:false"

# Machine-readable:
dhis2 --json messaging list

The CLI table colors the read column (unread bold-yellow) and the type column (TICKET / SYSTEM / VALIDATION_RESULT in magenta, PRIVATE / DIRECT in blue).

Read-state + cleanup

dhis2 messaging mark-read <uid> [<uid> ...]
dhis2 messaging mark-unread <uid> [<uid> ...]
dhis2 messaging delete <uid>           # soft-delete for the calling user only

delete is always soft from the caller's perspective: other participants of the conversation keep their view. DHIS2 purges fully once every participant has deleted.

Library API

from dhis2w_core.client_context import open_client
from dhis2w_core.profile import profile_from_env

async with open_client(profile_from_env()) as client:
    me = await client.system.me()
    assert me.id is not None

    # Upload an attachment first.
    fr = await client.files.upload_file_resource(
        b"...",
        filename="report.pdf",
        domain="MESSAGE_ATTACHMENT",
    )

    # Send — returns a typed MessageConversation (BUGS.md #17 workaround).
    conversation = await client.messaging.send(
        subject="Report",
        text="Latest numbers attached.",
        users=[me.id],
        attachments=[fr.id],
    )

    # Read / reply / mark / delete.
    await client.messaging.reply(conversation.id, text="thanks")
    await client.messaging.mark_read(conversation.id)
    await client.messaging.delete_conversation(conversation.id)

BUGS.md #17 — the Location-header UID dance

POST /api/messageConversations returns 201 Created with the new UID on the Location header, NOT in the JSON envelope. Every other DHIS2 create endpoint carries response.uid inside the body. The accessor papers over this: send() extracts the UID from Location and GETs the conversation back, so callers receive a typed MessageConversation object the way they do from client.files.upload_document. See BUGS.md #17 for the full repro + upstream-fix preference.

Related wire quirks the accessor handles so callers don't have to:

  • Attachments must be {id} reference objects on send — bare UID strings produce a 500. Callers pass list[str]; the accessor wraps.
  • The reply endpoint (POST /api/messageConversations/{uid}) takes text/plain body on v42, not JSON — a JSON payload gets stored verbatim as the message text. reply() encodes its text argument as plain UTF-8 bytes.