Skip to content

Plugin fields ​

A plugin can ask the technician for extra information in the field app without any app code. You declare the fields in plugin.json. Core ships the declarations to the app, validates what comes back, stores it and shows it on the console job sheet.

Field app fields ​

Declare fields under mobile.fields:

json
{
  "mobile": {
    "entry": null,
    "fields": [
      {
        "key": "docket_number",
        "label": "Disposal docket number",
        "type": "text",
        "entity": "job",
        "where": "completion",
        "required": true,
        "max_length": 80,
        "placeholder": "DKT-00000",
        "help": "Printed on the docket the facility gives you."
      },
      { "key": "volume_litres", "label": "Volume disposed", "type": "number", "where": "completion", "min": 0, "max": 100000, "unit": "L" },
      { "key": "docket_photo", "label": "Photo of the docket", "type": "photo", "where": "completion" },
      { "key": "gate_code", "label": "Gate code", "type": "text", "entity": "site" },
      { "key": "sludge_depth_cm", "label": "Sludge depth", "type": "number", "entity": "asset", "min": 0, "max": 500, "unit": "cm", "job_types": ["pump_out"] }
    ]
  }
}
PropertyMeaning
keysnake_case, unique per entity.
labelWhat the technician sees.
typetext, textarea, number, boolean, select, date or photo.
entityWhat the value belongs to: job (default), site (the property) or asset (the onsite system).
wherejob: on the job sheet at any time (default). completion: in the "Before you complete" section.
requiredMust be filled before the job can be completed. Applies to the job's own fields and to its site and asset.
optionsFor select: a list of strings or { "value", "label" } objects.
max_length, min, max, unit, placeholder, helpType-specific limits and hints.
job_typesLimit the field to some job types, for example ["pump_out"].

A bad declaration (unknown type, repeated key, a select without options) makes the plugin fail to load with a message that names the field.

Types in the app ​

  • text, textarea: text box. max_length defaults to 255 and 4000.
  • number: decimal keypad, with the unit beside it. min and max are checked.
  • boolean: Yes and No choices. Tapping the chosen answer again clears it. "No" counts as an answer for required.
  • select: one button per option.
  • date: YYYY-MM-DD, with a Today shortcut.
  • photo: camera or library. The photo goes through the normal photo upload queue, filed under the field key plugin:<slug>:<key>. The value is the app's local photo id.

An app version that does not know a type shows the value as read-only text and never blocks a job on it.

Changing the declaration per organization ​

The mobile.fields filter receives a plugin's declared fields (normalised) and its slug. The bundled disposal-dockets plugin uses it to make the docket number required only when the organization's setting says so:

php
$api->addFilter('mobile.fields', function (array $fields, string $slug): array {
    return array_map(function (array $field): array {
        if ($field['key'] === 'docket_number') {
            $field['required'] = (bool) $api->setting('require_on_completion', true);
        }
        return $field;
    }, $fields);
}, 10, 2);

How values sync ​

Pull. The mobile sync pull returns, for each plugin the organization has enabled and that declares fields:

json
"extensions": {
  "disposal-dockets": {
    "name": "Disposal docket numbers",
    "version": "1.1.0",
    "fields": [ { "key": "docket_number", "type": "text", "entity": "job", "where": "completion", "required": true } ],
    "values": { "job": { "<job id>": { "docket_number": "DKT-20407" } } }
  }
}

values covers the jobs, properties and systems on the device. The mobile.pull.payload filter still runs afterwards.

Push. The app queues a plugin_fields_saved event whenever the technician changes a field. Edits to the same record before a sync are folded into one event:

json
{
  "event_type": "plugin_fields_saved",
  "occurred_at": "2026-11-12T01:20:00Z",
  "payload": {
    "plugin": "disposal-dockets",
    "entity": "job",
    "entity_id": "<job id>",
    "values": { "docket_number": "DKT-31337", "volume_litres": 3500 }
  }
}

Core then:

  • validates every value against the declaration (type, range, options, length). A bad value fails validation with events.N.payload.values.<key>;
  • stores it in plugin_field_values (organization, plugin, record type, record id, key). The record must belong to the organization, or the event is ignored;
  • keeps the newest capture: an older occurred_at never replaces a newer one, and a null value clears the field;
  • ignores events for a plugin the organization has not enabled (result status ignored), and drops keys the plugin no longer declares (listed under dropped in the result). Neither blocks the sync;
  • blocks job_status_changed to completed or completed_pending_review with a validation error while a required field is empty. Values sent earlier in the same batch count.

Replays are safe: events are idempotent on idempotency_key.

Hooks ​

HookUse
mobile.fields (filter)Change declared fields per organization.
mobile.field_values (filter)See values before they are saved. Throw a ValidationException to veto.
mobile.fields_saved (action)Values were saved. Use it to copy them into the plugin's own tables.

disposal-dockets listens to mobile.fields_saved and keeps its disposal_dockets table in step, so its PDF block and route keep working.

Where values show ​

Saved values for a job appear on the console job sheet as extra facts, labelled with the field's label, for enabled plugins only. Disabling a plugin hides its fields and values and keeps the data.

What happens when things change ​

  • A plugin is switched off: the next pull no longer lists it. The app stops showing its fields. Values waiting to send are sent anyway and ignored by the server.
  • A field is removed or its type or options change: before each push the app drops waiting values that no longer fit the declaration, instead of failing the whole sync.
  • Unsent values survive a pull. A value the technician captured is never replaced by the server's older copy until it has been sent.
  • An unknown field type is shown read-only.

Themes in the field app ​

The pull also carries theme: the organization's resolved tokens and the logo address. The app applies brand, frame, ink, canvas, surface, line and radius, saves them on the device, and checks contrast. A colour that would make text unreadable is darkened a little, or ignored and the app keeps its own.

A$125 a month, or A$9,450 once with the source code. Contact us.