--- name: one-line-port-schedules-scraper description: Read Ocean Network Express (ONE) berth schedules as structured records via the Apify Actor arman-bd/one-line-port-schedules-scraper. Returns one record per vessel call at a port with arrival/berthing/departure times and status, cargo/document/VGM/inland cut-offs, ONE's coded delay reason, and optionally the calling vessel's own particulars (tonnage, IMO, flag, class). Resolves port names to UN/LOCODEs on request. Use for schedule-slippage tracking, booking-cut-off alerts, berth utilisation and vessel due diligence on ONE-operated services. Not for other carriers, container/BL track-and-trace, or historical (pre-window) schedules. --- # ONE Line Port Schedules & Cut-offs Scraper Apify Actor `arman-bd/one-line-port-schedules-scraper`. Name a port (by UN/LOCODE or by name) and a date window, get one dataset record per vessel call in that window. It runs without credentials, and with no input at all it returns Rotterdam's next two weeks. ## When to use it - Schedule-slippage analysis: `vesselDelayReasonCode` is a coded delay reason, unusual for a public schedule source, that supports real attribution rather than just a moving ETA. - Booking cut-off monitoring: alert before `cargoCutOffDateTime`, `documentCutOffDateTime` or `vgmCutOffDateTime` passes for cargo you have booked. - Berth-level utilisation: `yardCode` plus `berthingDateTime`/`departureDateTime` gives terminal occupancy for a port over a window. - Vessel due diligence for ONE-operated tonnage: `includeVesselDetails` merges GT/NT, IMO number, flag, class and build date into every call for that vessel. - Discovering a port's UN/LOCODE from a name, via `portNames`, before a scheduled or repeated pull that should use `portCodes` directly. ## When not to use it - Other ocean carriers. This Actor reads ONE's own schedule service only. - Container or bill-of-lading track-and-trace. Not covered by this Actor. - A date range outside what the source currently publishes. There is no historical archive; each run reads whatever window ONE's schedule service currently serves for that port. - Vessel particulars in isolation. `includeVesselDetails` enriches calls found while scraping a port's schedule; it is not a standalone vessel lookup. ## Call it ```js import { ApifyClient } from 'apify-client'; const client = new ApifyClient({ token: process.env.APIFY_TOKEN }); const run = await client.actor('arman-bd/one-line-port-schedules-scraper').call({ portCodes: ['NLRTM', 'DEHAM'], fromDate: '2026-08-22', toDate: '2026-09-05', vesselType: 'I', cargoNature: 'GENERAL_PURPOSE', maxRecords: 300, includeVesselDetails: false, }); const { items } = await client.dataset(run.defaultDatasetId).listItems(); const { value: summary } = await client .keyValueStore(run.defaultKeyValueStoreId) .getRecord('RUN_SUMMARY'); ``` One-shot over HTTP, when you want the rows back in the same request: ```bash curl -X POST "https://api.apify.com/v2/acts/arman-bd~one-line-port-schedules-scraper/run-sync-get-dataset-items?token=$APIFY_TOKEN" \ -H "Content-Type: application/json" \ -d '{"portCodes":["NLRTM"],"fromDate":"2026-08-22","toDate":"2026-09-05","maxRecords":200}' ``` The Actor is also exposed through Apify's MCP server as `arman-bd/one-line-port-schedules-scraper`, so an MCP-capable agent can call it with no extra wiring. ## Input | Field | Type | Required | Default | Notes | |---|---|---|---|---| | `portCodes` | string[] | no | `["NLRTM"]` | UN/LOCODEs, e.g. `NLRTM`, `DEHAM`. Case-insensitive, deduplicated, merged with whatever `portNames` resolves to. | | `portNames` | string[] | no | `[]` | Port names or partial names, 3+ letters each, resolved to UN/LOCODEs before scraping. A name matching several ports resolves to all of them. Unresolvable names are recorded in `RUN_SUMMARY.nameResolutionFailures` and do not stop the run. | | `fromDate` | string | no | today | `YYYY-MM-DD`. | | `toDate` | string | no | `fromDate` + 14 days | `YYYY-MM-DD`. Must not be before `fromDate`, or the run refuses to start. | | `vesselType` | string | no | `I` | Passed through to the source as-is. `I` (inbound) is the only confirmed value. | | `cargoNature` | string | no | `GENERAL_PURPOSE` | Passed through to the source as-is. `GENERAL_PURPOSE` is the only confirmed value. | | `maxRecords` | integer | no | `200` | Total berth calls saved across every port. `0` means no limit. A non-integer or negative value refuses to start rather than being read as unlimited. | | `includeVesselDetails` | boolean | no | `false` | Merges tonnage/IMO/flag/class/build-date into every row for each distinct vessel found. One extra request per distinct vessel, capped at 40 per run. | **There is no pagination on the source.** One request per port returns every call in the window at once, so `maxRecords` is the only depth control, and it is a run-wide budget, not a per-port one: ports are read in the order given, and once the cap is hit the remaining ports in the list are skipped entirely (logged, not silently dropped). ## Output One record per vessel call. Every field is present on every record; a value the source has not set is `null` rather than the key being missing, so tabular exports (CSV/Excel) stay flat. | Field | Type | Notes | |---|---|---| | `portCode` | string | The UN/LOCODE the call belongs to, as requested (uppercased). | | `portName` | string \| null | Port name as ONE's schedule service resolved it. | | `vvdName` | string \| null | Voyage identifier: vessel + voyage number + direction. | | `vesselName`, `vesselCode` | string \| null | Vessel name and ONE's internal vessel code. | | `vesselDetailDisplay` | string \| null | `Y`/`N` flag from the source; whether it shows vessel details on its own page. | | `scheduleVoyageNumber`, `scheduleDirectionCode` | string \| null | Voyage number and direction code. | | `yardName`, `yardCode` | string \| null | Terminal name and code. | | `vesselServiceLaneCode`, `vesselServiceTypeCode` | string \| null | ONE's own service/trade-lane codes. | | `arrivalDateTime`, `arrivalStatus` | string \| null | ETA/ATA (`YYYY-MM-DD HH:mm`) and status (`A` actual, `KK` estimated, source-coded). | | `berthingDateTime`, `berthingStatus` | string \| null | ETB/ATB and status, same coding. | | `departureDateTime`, `departureStatus` | string \| null | ETD/ATD and status, same coding. | | `cargoCutOffDateTime`, `documentCutOffDateTime`, `vgmCutOffDateTime`, `inlandCutOffDateTime` | string \| null | The four booking cut-offs. `inlandCutOffDateTime` is `null` on most calls. | | `vesselDelayReasonCode`, `vesselDelayReason` | string \| null | Coded delay reason and its plain-text explanation. Both `null` when the call is not flagged as delayed. | | `terminalNameMaskFlag` | string \| null | Source flag for whether the terminal name is masked. | | `netTonnage`, `grossTonnage` | string \| null | NT/GT as the source reports them (decimal strings). `null` unless `includeVesselDetails` is on and the lookup succeeded. | | `portRegistry` | string \| null | Vessel's port of registry (flag state), when fetched. | | `ownerName` | string \| null | Registered owner/charterer as the source reports it, when fetched. | | `callSign` | string \| null | Vessel call sign, when fetched. | | `carrier` | string \| null | Operating carrier, when fetched. | | `builtOn` | string \| null | Build date (`YYYY-MM-DD`), when fetched. | | `lloydsNo` | string \| null | IMO number, when fetched. | | `registryCode` | string \| null | Flag registry code (e.g. `LR`), when fetched. | | `classNo` | string \| null | Classification society, when fetched. | | `age` | string \| null | Vessel age in years, when fetched. | | `vesselDetailsFetched` | boolean | `true` exactly when the eleven fields above were fetched for this row. | | `fromDate`, `toDate` | string | The window this run used (after defaults were applied). | | `vesselType`, `cargoNature` | string | The filters this run used. | | `scrapedAt` | string | Run timestamp, ISO 8601. | ## RUN_SUMMARY Written to the run's key-value store under the key `RUN_SUMMARY`. ```json { "portsRequested": ["NLRTM", "DEHAM"], "portsFromExplicitCodes": ["NLRTM", "DEHAM"], "portsFromNameSearch": [], "nameResolutionFailures": [], "portsFailed": 0, "failures": [], "portResults": [ { "portCode": "NLRTM", "resolvedPortName": "ROTTERDAM, NETHERLANDS", "scheduleLines": 117, "saved": 117 }, { "portCode": "DEHAM", "resolvedPortName": "HAMBURG, HH, GERMANY", "scheduleLines": 83, "saved": 83 } ], "scheduleLinesSaved": 200, "includeVesselDetails": false, "vesselLookupsAttempted": 0, "vesselLookupFailures": [], "filters": { "fromDate": "2026-08-22", "toDate": "2026-09-05", "vesselType": "I", "cargoNature": "GENERAL_PURPOSE", "maxRecords": 200 }, "finishedAt": "2026-08-22T09:12:03.041Z" } ``` On a run that never started (a malformed date, an out-of-range `maxRecords`, or no usable port at all), the shape is smaller and carries `configError` instead: ```json { "configError": "\"fromDate\" must be a real date as YYYY-MM-DD, got \"not-a-date\".", "failures": [], "finishedAt": "2026-08-22T09:12:03.041Z" } ``` `portResults` is the place to check whether `maxRecords` bound the run before every requested port was read: a port missing from `portResults` was skipped because the cap was already reached, and that skip is also a log line, not silent. `scheduleLinesSaved` equal to `filters.maxRecords` is the tell. ## Behaviour to plan around - **An unrecognised port code is a legitimate empty result, not a failure.** The source answers `{"scheduleLines":[]}` for a code it does not know, the same as a code it knows with nothing in the window, the two are not distinguishable from the response alone. Zero rows for one port is not itself evidence of a typo. - **`portNames` resolution failures never sink the run.** An unresolvable name is recorded in `nameResolutionFailures` and the run continues with whatever `portCodes` or other names produced. The run only ends in error when nothing usable resolves at all. - **There is no pagination.** One request per port returns the whole window. Raise `maxRecords` (or set it to `0`) to keep more of a large window; there is no deeper page to fetch. - **`maxRecords` bounds the whole run, not each port.** Ports are read in the order listed; once the cap is hit, later ports in the list are skipped and named in the log, not silently omitted. - **`includeVesselDetails` is opt-in and vessel-scoped, not row-scoped.** One lookup per distinct vessel, cached and reused across every one of that vessel's calls in the run, capped at 40 distinct vessels. Past the cap, later vessels simply keep `vesselDetailsFetched: false` rather than erroring. - **Status codes and cut-offs move as a call approaches and completes.** Re-run and key on `vvdName` + `portCode` + `berthingDateTime` to build a trajectory rather than treating one run as final, the same call re-queried later can show a different `arrivalStatus`, a shifted `berthingDateTime`, or a newly populated `vesselDelayReasonCode`. - **A malformed date or an unusable `maxRecords` ends the run immediately**, before any network call, with the reason in `RUN_SUMMARY.configError`. This is the one case where a nonzero exit code is expected. - **Retries (3 attempts, linear backoff)** cover transient failures per port and per vessel lookup; a broken port ends only that port's contribution and is named in `RUN_SUMMARY.failures`, while every other requested port still runs. ## Recipes **Daily cut-off watch for a booked vessel.** Narrow window, no vessel enrichment needed. ```json { "portCodes": ["NLRTM"], "fromDate": "2026-08-22", "toDate": "2026-08-29", "maxRecords": 100 } ``` Filter the dataset on `vesselName` client-side and alert on `cargoCutOffDateTime`. **Multi-port slippage sweep.** A trade lane's main calls, two weeks out, with delay codes. ```json { "portCodes": ["NLRTM", "DEHAM", "BEANR", "GBSOU"], "fromDate": "2026-08-22", "toDate": "2026-09-05", "maxRecords": 600 } ``` Group by `vesselDelayReasonCode` where non-null to see which reason dominates across the lane. **Vessel due diligence for everything calling a port this week.** ```json { "portCodes": ["NLRTM"], "fromDate": "2026-08-22", "toDate": "2026-08-29", "includeVesselDetails": true, "maxRecords": 150 } ``` Deduplicate on `vesselCode` client-side for a one-row-per-vessel particulars table; the dataset itself stays one row per call by design.