Schema version: 1.0  ·  Status: Stable  ·  First shipped: Wave 14AE

Data Export Schema v1

The Data Feed writes one JSON file (entries.json) and four CSVs to a folder you choose. This page documents the JSON shape. It's the contract for both the scheduled folder drop (Build #1) and the outbound webhooks (Build #3, in progress). If you build a Power BI dashboard, an Excel Power Query model, or a Python ingest script against v1, we promise to never break it.

Compatibility promise (the never-break list)
On this page

Full example

One entry. The entries.json file is a JSON array of these objects.

{
  "schema_version": "1.0",
  "id": 1234,
  "ref_number": "Q-2026-001",
  "description": "FAB,SS304,RU,PNT",
  "comments": "Customer approved spec on Mar 15. Ship-by Apr 30.",
  "status": "In Production",
  "pipeline_id": "fabrication",
  "tags": {
    "Material": ["Stainless Steel 304"],
    "Process":  ["Welding", "Polishing"],
    "Priority": ["High"]
  },
  "key_fields": {
    "customer_name": "Acme Corp",
    "due_date": "2026-04-30",
    "priority": "High",
    "assigned_to": "alice",
    "quantity": "100",
    "estimated_value": "12500",
    "po_number": "PO-78421",
    "part_number": "BR-1042",
    "actual_hours": "38.5",
    "estimated_hours": "40",
    "estimated_material_cost": "2400",
    "actual_material_cost": null,
    "labor_rate": "95",
    "quote_valid_until": null,
    "quote_outcome": "Won",
    "drawing_revision": "Rev B",
    "lost_reason": null
  },
  "custom_fields": {
    "Special Instructions": "Hand-finish edges",
    "QC Inspector": "bob"
  },
  "quote_line_items": [
    { "description": "CNC milling — bracket",
      "quantity": 100,
      "unit_price": 125.00,
      "material_cost": 24.00,
      "markup_percent": null,
      "line_total": 12500.00 }
  ],
  "material_requirements": [
    { "material": "Stainless Steel 304",
      "specification": "1/4\" x 6\"",
      "quantity_needed": 50,
      "unit": "ft",
      "is_allocated": true,
      "notes": "" }
  ],
  "quality_notes": [],
  "stage_transitions": [
    { "stage": "Quote",         "timestamp": "2026-03-01T10:00:00.0000000Z" },
    { "stage": "Approved",      "timestamp": "2026-03-05T14:30:00.0000000Z" },
    { "stage": "In Production", "timestamp": "2026-03-08T08:00:00.0000000Z" }
  ],
  "attachments": [
    { "file_name": "drawing_v3.pdf",
      "stored_file_name": "a1b2c3d4_drawing_v3.pdf",
      "file_size_bytes": 482910,
      "category": "Drawing",
      "added_at": "2026-03-02T09:14:00",
      "added_by": "alice" }
  ],
  "audit_summary": {
    "change_count": 7,
    "last_changed_at": "2026-03-15T11:30:00.0000000Z",
    "last_changed_by": "alice"
  },
  "work_state": {
    "is_on_hold": false,
    "hold_reason": "",
    "hold_notes": "",
    "is_rework": false,
    "rework_from_stage": "",
    "rework_reason": "",
    "rework_notes": "",
    "rework_returned_by": "",
    "rework_returned_at": "",
    "active_work_by": "alice",
    "active_work_started_at": "2026-03-15T08:00:00"
  },
  "created_at": "2026-03-01T10:00:00.0000000Z",
  "created_by": "alice",
  "last_modified_at": "2026-03-15T11:30:00.0000000Z",
  "last_modified_by": "alice",
  "revision_number": 7,
  "is_archived": false,
  "is_deleted": false,
  "deleted_at": null,
  "deleted_by": ""
}

Root entry fields

FieldTypeDescription
schema_versionstringAlways "1.0" for v1 output. Pin your consumer to this value and bail loudly on a mismatch.
idintegerStable internal entry ID. Use this as the join key when relating to time_entries.csv (EntryId column) or any other future per-entry table.
ref_numberstringHuman-visible reference number, e.g. "Q-2026-001" or "WO-2024-001". Unique among non-deleted entries; format determined by the project's job-number rules.
descriptionstringAuto-generated short descriptor from tag abbreviations (e.g. "FAB,SS304,RU") or a user override. May be empty.
commentsstringFree-text notes. Multi-line. May contain newlines.
statusstringCurrent pipeline stage name (e.g. "In Production"). Empty string if the project has no pipeline configured.
pipeline_idstringEmpty string for the project's default/legacy pipeline; otherwise the named pipeline (e.g. "fabrication", "service").
created_atISO 8601 UTCWhen the entry was first created. Format: "yyyy-MM-ddTHH:mm:ss.fffffffZ".
created_bystringDisplay name of the creator. AD prefix (e.g. "DOMAIN\\") is stripped.
last_modified_atISO 8601 UTCTimestamp of the most recent edit. Equals created_at if never edited.
last_modified_bystringDisplay name of the last editor.
revision_numberintegerEdit counter. Increments by 1 on each content edit. Used internally for optimistic concurrency. 0 = never edited since creation.
is_archivedbooleantrue if the entry is archived (closed-the-books) but not deleted.
is_deletedbooleantrue if soft-deleted. Soft-deleted entries are still emitted so consumers can audit the deletion history.
deleted_atISO 8601 UTC | nullWhen the entry was soft-deleted. null for non-deleted entries.
deleted_bystringWho soft-deleted it. Empty for non-deleted entries.

Tags

Object keyed by category name. Each value is an array of selected tag values from that category. Multi-select categories yield multiple values; single-select yields one.

"tags": {
  "Material":     ["Stainless Steel 304"],
  "Process":      ["Welding", "Polishing"],
  "Priority":     ["High"]
}

Empty object {} when an entry has no tags. Categories that exist in the project but aren't applied to this entry are omitted (they don't appear with empty arrays).

Key fields (built-in)

Object containing every built-in "Key Field" defined in GlyphFex. Every key always appears — values that aren't populated emit null. This stabilizes column inference in Power BI / Excel Power Query (consumers see the same shape on every entry).

Values are typically string-encoded numbers (e.g., "95" for a labor rate). That's intentional — the project model stores key fields as a string-keyed JSON dict, and individual fields can carry text, numeric, or date values depending on configuration. Coerce to the type you want in your consumer query (Power Query: Number.From, Date.From).

KeyBuilt-in typeDescription
customer_nameTextCustomer or client name.
due_dateDateExternal customer-imposed deadline. Format: "yyyy-MM-dd".
priorityDropdownOne of: Low, Medium, High, Critical.
assigned_toTextUsername of the person responsible.
quantityNumberNumber of units.
estimated_valueNumberQuoted price or budget in dollars.
po_numberTextCustomer purchase order number.
part_numberTextPart or drawing number for repeat-job grouping.
actual_hoursNumberTotal hours actually spent on the job.
estimated_hoursNumberEstimated labor hours.
estimated_material_costNumberEstimated material cost in dollars.
actual_material_costNumberActual material cost in dollars.
labor_rateNumber$/hr labor rate used for cost calculations.
quote_valid_untilDateQuote expiry date. Format: "yyyy-MM-dd".
quote_outcomeDropdownPending, Won, Lost, or Expired.
drawing_revisionTextDrawing revision letter or number, e.g. "Rev B" or "3".
lost_reasonDropdownOne of: Too expensive, Lost to competitor, Too slow / lead time, Customer cancelled, Specs changed, No response, Other.

Forward compatibility: when a new built-in key field is added in a future GlyphFex release, it joins this list automatically. Existing entries emit it with null; new entries that populate it emit the value. No schema-version bump required — this is an additive change v1 explicitly allows.

Custom fields

Object keyed by the field name (the user-visible label). Values are the user-entered text. Empty object {} when an entry has no custom-field values.

Custom field names are project-specific (configured by the admin) and may change. Values are stored as strings regardless of the underlying field type (Text, LargeText, Dropdown, Date, Number). Consumers should coerce based on the field's configured type if they care.

Quote line items

Array of line items in the order the user entered them.

FieldTypeDescription
descriptionstringItem description.
quantitynumberNumber of units.
unit_pricenumberPrice per unit in dollars.
material_costnumberMaterial cost for the line (used for margin math).
markup_percentnumber | nullIf non-null, unit_price = material_cost × (1 + markup_percent/100). Null = unit price entered directly.
line_totalnumberComputed: quantity × unit_price.

Material requirements

FieldTypeDescription
materialstringMaterial name (e.g., "Aluminum 6061-T6").
specificationstringSize/spec (e.g., "1/2\" x 4\" x 12\"").
quantity_needednumberHow much is needed.
unitstringOne of: pcs, ft, in, m, mm, kg, lb, sheets, bars, tubes, plates, rolls, ea.
is_allocatedbooleantrue if the material has been allocated/consumed.
notesstringFree-text.

Quality notes (NCR records)

Non-conformance reports for ISO 9001 quality tracking. Array; empty for entries with no quality issues.

FieldTypeDescription
created_atISO 8601 stringWhen the NCR was opened.
created_bystringWho opened the NCR.
typestringOne of: Internal, Customer, Supplier.
descriptionstringWhat went wrong.
dispositionstringOne of: Rework, Scrap, Accept As-Is, Return to Supplier, Pending Review.
root_causestringFree-text root-cause analysis (ISO 9001 §10.2.1).
severitystringOne of: Minor, Major, Critical.
affected_quantityintegerNumber of non-conforming parts.
cost_impactnumberEstimated cost impact in dollars.
corrective_actionstringWhat was/will be done.
corrective_action_due_datestringFormat: "yyyy-MM-dd". Empty when no deadline.
is_resolvedbooleantrue when the NCR is closed.
resolved_atISO 8601 stringWhen closed. Empty if open.
effectiveness_verifiedbooleanISO 9001 §10.2.1e: did the corrective action actually prevent recurrence?
effectiveness_verified_atISO 8601 stringWhen verified.
effectiveness_verified_bystringWho verified.

Stage transitions

Chronological pipeline history. Each transition records the stage entered and the timestamp. For non-linear workflows (where a job revisits a stage), every visit is preserved as a separate entry.

"stage_transitions": [
  { "stage": "Quote",         "timestamp": "2026-03-01T10:00:00" },
  { "stage": "Approved",      "timestamp": "2026-03-05T14:30:00" },
  { "stage": "In Production", "timestamp": "2026-03-08T08:00:00" }
]

Note on timestamps: stage_transitions[].timestamp values are emitted as written by the GlyphFex client at the time of the transition. They may include or omit the Z suffix depending on which version of GlyphFex recorded them. Newer transitions write ISO 8601 round-trip format. Treat them as ISO 8601 strings; Date.From in Power Query handles both forms.

Attachments

Metadata only — no file bytes and no absolute paths. Customers with access to the project's <projectname>_Attachments/ folder can locate the file at <projectname>_Attachments/<stored_file_name>.

FieldTypeDescription
file_namestringOriginal filename as the user uploaded it (e.g., "drawing_v3.pdf").
stored_file_namestringGUID-prefixed name in the attachments folder (e.g., "a1b2c3d4_drawing_v3.pdf").
file_size_bytesintegerFile size in bytes.
categorystringOne of: Drawing, Setup Sheet, CNC Program, Photo, Quote, Purchase Order, Inspection Report, Spec Sheet, Other (or empty for uncategorized).
added_atISO 8601 stringWhen the file was attached.
added_bystringWho attached it.

Audit summary

Per-entry summary of the audit trail. Always present (even on never-edited entries, with change_count: 0). The full audit trail is not embedded per entry — it would inflate file size on busy projects. A separate audit_trail.csv with full per-change history is planned for a future build.

FieldTypeDescription
change_countintegerNumber of audit records for this entry.
last_changed_atISO 8601 UTC | nullWhen the most recent change occurred. Null if never changed.
last_changed_bystringWho made the most recent change. Empty if never changed.

Work state

Live state for shop-floor coordination — hold, rework, and active-work indicators.

FieldTypeDescription
is_on_holdbooleanThe entry is paused waiting on something (material, approval, etc.).
hold_reasonstringReason code from the project's configurable list.
hold_notesstringFree-text reason notes.
is_reworkbooleanThe entry was sent back from a later stage for rework.
rework_from_stagestringWhich stage sent it back.
rework_reasonstringReason code.
rework_notesstringFree-text reason notes.
rework_returned_bystringUsername of who sent it back.
rework_returned_atISO 8601 stringWhen it was sent back.
active_work_bystringUsername of the worker currently on the job. Empty when idle.
active_work_started_atISO 8601 stringWhen the active worker started.

Conventions

Casing

All field names are snake_case. Power BI Power Query, pandas, and most in-house ERPs handle snake_case idiomatically.

Timestamps

Top-level created_at / last_modified_at / last_changed_at / deleted_at are ISO 8601 UTC with the Z suffix and 7 fractional-second digits, e.g. "2026-04-30T14:22:00.0000000Z".

Embedded timestamps inside arrays (stage_transitions, quality_notes, attachments) and inside work_state are passed through as the client wrote them; they are ISO 8601 strings but may not always carry the Z suffix. Coerce via Date.From / datetime.fromisoformat.

Date-only fields

due_date, quote_valid_until, and corrective_action_due_date use "yyyy-MM-dd" with no time component.

Encoding

entries.json is written as UTF-8 without BOM (RFC 8259 forbids the BOM in JSON). The companion CSV files (entries.csv, quotes.csv, customers.csv, time_entries.csv) are UTF-8 with BOM — Excel needs the BOM to detect UTF-8.

Atomic write

GlyphFex writes each file to <name>.tmp first, then renames it over the destination. A consumer scanning the folder will never see a half-written file. If you scan the folder yourself, filter *.tmp out.

Lock file

In multi-user team mode, GlyphFex elects one writer per folder via a hidden .feed.lock sentinel. The file is set FileAttributes.Hidden so Power BI's "Get Data → Folder" connector doesn't enumerate it by default. If your tool ignores the Hidden attribute, filter .feed.lock out of your folder query.

What we don't do

We don't sync, we don't push to your tools, and we don't accept inbound writes. You point your tool at the folder; we keep it fresh. If the folder is on a network share, multiple GlyphFex installs coordinate via the lock file so the writes don't race. If you want event-driven delivery instead of folder-scan polling, see Build #3 outbound webhooks (in progress).

Wiring it up

Power BI

  1. Get Data → Folder → select your Data Feed folder.
  2. Filter rows: Extension equals ".csv" for the CSVs, or ".json" for entries.json.
  3. For entries.json: click "Combine & Transform Data," expand the JSON list, then expand the columns you want. Power BI handles snake_case fields natively.
  4. Schedule a refresh in Power BI Service or use Power BI Desktop's manual refresh.

Excel Power Query

Same flow as Power BI — Data → Get Data → From File → From Folder. Excel reads CSVs with the BOM correctly; for entries.json use "From File → From JSON" and expand the list.

Python

import json, pandas as pd

with open("entries.json", encoding="utf-8") as f:
    entries = json.load(f)

assert all(e["schema_version"] == "1.0" for e in entries), "schema mismatch"

# Flatten the key_fields object into top-level columns
df = pd.json_normalize(entries, sep="_")
print(df[["ref_number", "key_fields_customer_name",
         "key_fields_estimated_value", "status"]].head())

Joining time entries to entries

time_entries.csv has both an EntryId column (integer FK, matches entries.jsonid) and an EntryRefNumber column (the human-visible ref number). Use whichever is more convenient — both are stable and join cleanly.


Questions? Email support@glyphfex.com. Found a documentation issue? Open an issue.