How to Push DHL Express Tracking Updates to MySQL and Slack
Build a scheduled Spojit workflow that reads open shipments from MySQL, checks each one against DHL Express, writes the latest status back to the row, and posts delivered shipments to a Slack channel.
What This Integration Does
If you store outbound shipments in a MySQL table, you usually have a column for the DHL Express tracking number but no reliable way to keep the status fresh. This workflow closes that gap. On a schedule, it pulls every shipment that is still in transit, calls DHL Express for the current tracking status of each one, updates the matching row, and announces any shipment that has just been delivered to a Slack channel so your operations team sees it without logging into a carrier portal.
The run model is a polling loop. A Schedule trigger fires on a cron expression you set. A Connector node reads the open shipments from MySQL, a Loop iterates over them, and inside the loop each shipment is tracked with DHL Express and its row is updated. Because the workflow filters on a status column, it is naturally idempotent: once a row is marked delivered it is no longer returned by the read query, so re-runs never re-process or re-notify a finished shipment. If DHL has no new movement, the row is rewritten with the same status and nothing is posted to Slack.
Prerequisites
- A MySQL connection added under Connections, with read and write access to your shipments table.
- A shipments table that includes at least a primary key, a DHL tracking number column, and a status column. This article assumes a table named
shipmentswith columnsid,tracking_number,status, andlast_checked_at. - A DHL Express connection (API key credentials) added under Connections.
- A Slack connection with permission to post to your target channel, and the channel ID or name you want delivery alerts to land in.
- Tracking numbers already populated in the
tracking_numbercolumn. This workflow reads existing shipments rather than creating them.
Step 1: Add a Schedule trigger
Open the Workflow Designer, add a Trigger node, and set its type to Schedule. A Schedule trigger uses a 5-field Unix cron expression plus an IANA timezone. For a check every 30 minutes during business hours, use */30 8-18 * * 1-5 with timezone Australia/Sydney. A single trigger can hold more than one schedule, so you can add a separate overnight cadence later if you want. The trigger output is { scheduledAt }, which you do not need to map downstream here.
Step 2: Read open shipments from MySQL
Add a Connector node in Direct mode, choose the MySQL connector, and select the execute-query tool. Direct mode keeps this step deterministic with no AI cost. In the query field, select only shipments that are still in flight and cap the batch so a single run stays fast:
SELECT id, tracking_number
FROM shipments
WHERE status NOT IN ('Delivered', 'Cancelled')
AND tracking_number IS NOT NULL
ORDER BY last_checked_at ASC
LIMIT 100
Ordering by last_checked_at ASC means the least recently checked shipments are processed first, so nothing is starved across runs. Name the node output something like open_shipments so you can reference its rows in the next step.
Step 3: Loop over each open shipment
Add a Loop node in ForEach mode and point it at the rows returned above, for example {{ open_shipments.data.rows }} (use the row array your MySQL node returns). Each iteration exposes the current row, which you can reference as {{ item }}. Everything in Steps 4 through 6 goes inside this loop body so it runs once per shipment. Keep the loop body small and deterministic; the read query in Step 2 already limits how many shipments a single run handles.
Step 4: Track the shipment with DHL Express
Inside the loop, add a Connector node in Direct mode, choose the DHL Express connector, and select the track-shipment tool. Map the inputs from the current row:
trackingNumber:{{ item.tracking_number }}levelOfDetail:shipment(returns the overall shipment status rather than piece-level events, which is all you need to update the row)
Name this node output tracking. The response carries the carrier's current status for the shipment. Use a Transform node next if you want to normalize the DHL status text into your own status vocabulary (for example mapping the carrier's delivered indicator to the literal Delivered value your table uses). Store the normalized value in a variable such as {{ new_status }}.
Step 5: Update the shipment row in MySQL
Still inside the loop, add a Connector node in Direct mode using the MySQL connector and the update-rows tool. This writes the latest status back to the exact row by primary key. Configure the fields:
table:shipmentsset: a JSON object with the columns to update, for example:
{
"status": "{{ new_status }}",
"last_checked_at": "{{ tracking.checkedAt }}"
}
where:id = ?params:[ {{ item.id }} ]
Using a parameterized where clause with ? placeholders keeps the update scoped to one row and avoids quoting issues. If you do not have a timestamp value to write, you can update only the status column.
Step 6: Post delivered shipments to Slack
After the update, add a Condition node that checks whether this shipment just reached the delivered state, for example {{ new_status }} equals Delivered. On the true branch, add a Connector node in Direct mode using the Slack connector and the send-message tool. Map the inputs:
channel: your channel ID or name, for example#shipping-updatestext: a short, templated message such as:
Shipment {{ item.tracking_number }} (id {{ item.id }}) has been delivered by DHL Express.
Leave the false branch empty so shipments that are still moving generate no noise. Because the read query in Step 2 excludes anything already marked Delivered, each shipment can only cross this branch once, which prevents duplicate notifications across runs.
Tips
- Set
levelOfDetailtoshipmentrather thanallontrack-shipmentto keep responses lean when you only care about the overall status. - Keep the
LIMITin your read query aligned with your schedule frequency. A smaller batch on a tighter schedule spreads carrier calls evenly and keeps each run short. - Use a Transform node to map carrier status text to your own status values once, so both the MySQL update and the Slack condition read from a single normalized
{{ new_status }}variable. - If you want a daily summary instead of per-shipment pings, collect delivered rows in the loop and send one Slack
send-messageafter the loop completes.
Common Pitfalls
- Forgetting to exclude finished shipments in the read query. Without the
status NOT IN ('Delivered', 'Cancelled')filter, the workflow re-tracks and re-notifies the same shipments forever. - Schedule timezone confusion. Cron fields are evaluated in the IANA timezone you pick, so set it explicitly (for example
Australia/Sydney) rather than assuming UTC. - Updating with an unparameterized
whereclause. Always pass row identifiers throughparamswith?placeholders so a stray value cannot widen the update. - Missing or stale tracking numbers. Rows with a null
tracking_numberwill failtrack-shipment; the read query filters them out, so make sure that filter stays in place.
Testing
Before scheduling, temporarily tighten the read query to a single known shipment by adding AND id = ? with a test id, then use the Run button to execute the workflow once on demand. Confirm in the execution history that track-shipment returned a status, that the shipments row was updated, and that a Slack message posted only when the status resolved to delivered. You can ask Miraxa, the intelligent layer across your automation, to explain any failed step in context before you widen the query back to the full batch and enable the schedule.