USPS Mail Action Module
Shared USPS Informed Delivery processing used by both the shared mail runtime and the usps-mail plugin.
What lives here
| Module | Purpose |
|---|---|
analyze.py | Main orchestration pipeline for a single digest folder |
parse_digest.py | Parse the USPS HTML digest into structured mail metadata |
vision.py | Ask an OpenClaw agent to analyze each mailpiece image |
rules.py | User-managed importance rules stored in the workspace agent |
notify.py | Build and deliver recipient-specific notifications |
memory.py | Persist analysis history, workflow state, and monthly mail memory |
paths.py | Resolve workspace-owned USPS config/state locations |
End-to-end flow
flowchart TD
rule["mail_rules match"]
action["Named action<br/>process_usps_digest"]
plugin["Plugin tool<br/>usps_process_digest"]
folder["Digest folder"]
pipeline["process_digest(...)"]
parse["Parse HTML"]
vision["Phase 2A:<br/>vision"]
rules["Phase 2B:<br/>USPS rules"]
history["Save state"]
notify["Notify"]
memory["Write memory"]
handoff["Main follow-up"]
rule --> action --> folder
plugin --> folder
folder --> pipeline
pipeline --> parse --> vision --> rules --> history --> notify --> memory
memory --> handoff
The important split is:
- Phase 1 decides whether the incoming email is a USPS digest worth processing
- Phase 2A extracts structured facts from the scan images
- Phase 2B applies your personal USPS rules/config to those structured facts
USPS sub-phases
- Vision analysis (Phase 2A): the
vision_agentreads each scan image and returns normalized fields like sender, addressee, description, mail class, and address method - Post-processing (Phase 2B): the
workspace_agentappliesrules.json, usesconfig.jsonfor routing, persists history/state, and decides what notifications and memory writes happen next
Entry points
There are two normal ways into this package:
- Automatic mail pipeline:
mail_action_usps/register.pyregistersprocess_usps_digestwith the shared mail runtime, which downloads digest artifacts, callsprocess_digest(...), then hands a structured summary to another agent for any follow-up that still matters. - Interactive/manual tooling:
plugins/usps-mail/src/tools.pyexposes the same shared runtime functions as OpenClaw tools likeusps_process_digest,usps_lookup, andusps_rules.
That split is intentional:
libs/python/mail_action_usps/owns the USPS workflow itself- the shared mail runtime owns action dispatch, while integrating services own provider-specific ingestion
plugins/usps-mailowns the human/operator-facing tool surface
Agent boundaries
The USPS runtime deliberately splits durable data and work across agents.
| Agent role | What it owns |
|---|---|
workspace_agent | USPS config, rules, analysis history, workflow state, notification routing config |
vision_agent | Temporary scan-image staging area and the actual vision analysis call |
memory_agent | Long-term searchable markdown memory for processed mail |
workspace_agent
process_digest(...) requires a workspace_agent. That workspace is the operational home for USPS processing:
workspace/usps-mail/rules.json— classification rulesworkspace/usps-mail/config.json— notification routing configworkspace/memory/usps_analysis.json— accumulated analysis historyworkspace/memory/usps_state.json— dedup / workflow state
This is usually the mail agent workspace, because the mail pipeline owns ongoing USPS operations.
vision_agent
vision.py copies each scan image into:
~/.openclaw/agents/<vision_agent>/workspace/camera_captures/
Then it invokes:
openclaw agent --agent <vision_agent> --json --message ...
The vision agent returns structured JSON for each mailpiece, and the staging image is deleted immediately afterward. This keeps the vision work isolated from the rest of the USPS workflow.
memory_agent
memory.py writes monthly markdown summaries under:
~/.openclaw/agents/<memory_agent>/workspace/memory/mail/
That long-term memory is meant for the agent that should remember mail over time, which is typically the main agent rather than the mail-processing workspace.
What the pipeline actually does
For one digest folder, process_digest(...) runs these stages:
- Parse
body.htmland detect the delivery date. - Find scan images in the folder.
- Analyze each image through the configured vision backend.
- Apply user-managed importance rules.
- Persist structured history in
usps_analysis.json. - Write monthly mail memory markdown when enabled.
- Build and send notifications based on routing config.
- Update workflow state for dedup and last-processed tracking.
Two rule systems, not one
There are two different rule/config layers involved in automatic USPS handling:
| Layer | File | Purpose |
|---|---|---|
| Mail pipeline trigger rules | ~/.openclaw/services/fastmail-sse-config.json under mail_rules | Decide when to invoke the process_usps_digest action for an email |
| USPS classification rules | ~/.openclaw/agents/<workspace_agent>/workspace/usps-mail/rules.json | Decide how important each analyzed mailpiece is after vision |
That means:
mail_ruleswork at the email/message level- USPS
rules.jsonworks at the individual mailpiece image level
USPS rules structure
rules.py loads a versioned JSON file from the workspace agent:
~/.openclaw/agents/<workspace_agent>/workspace/usps-mail/rules.json
Shape:
{
"version": "1.2",
"rules": [
{
"_comment": "Former resident mail is low priority",
"addressee_contains": "former resident",
"importance": "low"
},
{
"_comment": "Tax documents are high priority",
"sender_contains": "county assessor",
"importance": "high"
}
]
}
Supported USPS rule operators are case-insensitive:
| Operator form | Meaning |
|---|---|
<field>_contains | substring must be present |
<field>_not_contains | substring must be absent |
<field>_equals | exact normalized string match |
<field>_not_equals | exact normalized string mismatch |
Supported fields:
addresseesenderdescriptionmail_classaddress_method
USPS rules are first match wins. The first matching rule overwrites the item importance and processing moves on.
Importance values
The USPS rule engine currently works with these importance levels:
urgenthighmediumlowjunkadunknown
What the rules evaluate
USPS rules run after scan-image vision has produced a normalized item like:
{
"sender": "County Assessor",
"addressee": "Jane Doe",
"description": "Property tax assessment notice",
"type": "scan",
"importance": "medium",
"mail_class": "First-Class Mail",
"address_method": "handwritten"
}
So the normal pattern is:
- vision extracts sender/addressee/description/mail class
- USPS rules override the default importance based on those fields
USPS notification config structure
Notification routing config lives at:
~/.openclaw/agents/<workspace_agent>/workspace/usps-mail/config.json
Shape:
{
"routing": {
"jeff": {
"channel": "discord",
"target": "<target-id>"
},
"nicole": {
"channel": "discord",
"target": "<target-id>"
},
"default": {
"channel": "discord",
"target": "<target-id>"
}
}
}
Current behavior from notify.py:
- mail addressed to Nicole / Eastside Improv routes to
nicole - joint Jeff + Nicole mail routes to
jeff - everything else falls back to
jeff - if no
routingblock exists, the runtime falls back toNOTIFY_CHANNEL/NOTIFY_TARGET
Only higher-priority pieces are notified directly; lower-priority items mainly remain in history/memory.
process_digest(...) runtime config
The main USPS pipeline function accepts these important knobs:
| Parameter | Purpose |
|---|---|
workspace_agent | required; owns USPS config, rules, history, and workflow state |
memory_agent | required when memory writing is enabled; owns long-term markdown memory |
vision_agent | required when vision_backend is auto; performs scan-image analysis |
vision_backend | auto, provided, or skip |
persist_analysis | whether to write usps_analysis.json |
write_memory | whether to write monthly mail memory |
send_notifications | whether to deliver USPS notifications |
update_workflow_state | whether to update dedup / last-processed state |
The function returns a structured summary with:
- date and mail counts
- per-image structured analysis
- notification plan and send results
- memory file path
- whether analysis/state persistence succeeded
FastMail companion behavior
When USPS processing runs from the FastMail mail pipeline, the call path is:
fastmail-sse email event → shared mail runtime rule match → process_usps_digest_action(...) → mail_action_usps.process_digest(...)
After that shared USPS work finishes, services/fastmail-sse/usps_integration.py creates an agent_handoff result with a structured JSON payload. That handoff tells the downstream agent:
- the mail agent already handled scan-image vision work
- direct USPS notifications were already routed
- durable mail memory may already have been written
- only non-notification follow-up is still needed
So the mail pipeline uses this package for the heavy USPS-specific work first, and only then asks another agent to reason about any broader follow-up.
Plugin companion behavior
The usps-mail plugin does not implement a separate USPS system anymore. Its Python entrypoint imports this package directly and exposes companion tools for:
- processing a digest folder
- searching saved USPS history
- listing/testing/updating rules
- viewing workflow status and summary stats
That keeps one USPS implementation in the shared action module while still making it available as an interactive tool surface.
Rule and notification ownership
USPS rule matching here is not the same as top-level mail mail_rules.
mail_rulesdecide when the mail pipeline should invoke USPS processingusps/rules.pydecides how important each individual mailpiece is after the digest is parsed and analyzed
Likewise, USPS notifications are handled here after classification, using routing rules from the workspace agent config.
Why this split exists
This package sits below both the service and plugin layers because USPS processing needs all of these properties at once:
- reusable by multiple entry points
- stateful across runs
- agent-aware for vision and memory
- separate from provider-specific mail ingestion
That makes libs/python/mail_action_usps/ the right place for the core workflow, while FastMail and the plugin stay thin adapters around it.