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.
- We will never remove a field that v1 emits.
- We will never change a field's type or units in v1.
- We will never change the meaning of a field once it has shipped.
- We may add new fields to v1 over time. Existing parsers should
ignore unknown fields. New built-in
key_fieldsautomatically appear withnullon entries that don't populate them. - Breaking changes ship as v2 with a new
schema_versionvalue, and v1 keeps emitting in parallel for at least one major release after.
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
| Field | Type | Description |
|---|---|---|
schema_version | string | Always "1.0" for v1 output. Pin your consumer to this value and bail loudly on a mismatch. |
id | integer | Stable 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_number | string | Human-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. |
description | string | Auto-generated short descriptor from tag abbreviations (e.g. "FAB,SS304,RU") or a user override. May be empty. |
comments | string | Free-text notes. Multi-line. May contain newlines. |
status | string | Current pipeline stage name (e.g. "In Production"). Empty string if the project has no pipeline configured. |
pipeline_id | string | Empty string for the project's default/legacy pipeline; otherwise the named pipeline (e.g. "fabrication", "service"). |
created_at | ISO 8601 UTC | When the entry was first created. Format: "yyyy-MM-ddTHH:mm:ss.fffffffZ". |
created_by | string | Display name of the creator. AD prefix (e.g. "DOMAIN\\") is stripped. |
last_modified_at | ISO 8601 UTC | Timestamp of the most recent edit. Equals created_at if never edited. |
last_modified_by | string | Display name of the last editor. |
revision_number | integer | Edit counter. Increments by 1 on each content edit. Used internally for optimistic concurrency. 0 = never edited since creation. |
is_archived | boolean | true if the entry is archived (closed-the-books) but not deleted. |
is_deleted | boolean | true if soft-deleted. Soft-deleted entries are still emitted so consumers can audit the deletion history. |
deleted_at | ISO 8601 UTC | null | When the entry was soft-deleted. null for non-deleted entries. |
deleted_by | string | Who 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).
| Key | Built-in type | Description |
|---|---|---|
customer_name | Text | Customer or client name. |
due_date | Date | External customer-imposed deadline. Format: "yyyy-MM-dd". |
priority | Dropdown | One of: Low, Medium, High, Critical. |
assigned_to | Text | Username of the person responsible. |
quantity | Number | Number of units. |
estimated_value | Number | Quoted price or budget in dollars. |
po_number | Text | Customer purchase order number. |
part_number | Text | Part or drawing number for repeat-job grouping. |
actual_hours | Number | Total hours actually spent on the job. |
estimated_hours | Number | Estimated labor hours. |
estimated_material_cost | Number | Estimated material cost in dollars. |
actual_material_cost | Number | Actual material cost in dollars. |
labor_rate | Number | $/hr labor rate used for cost calculations. |
quote_valid_until | Date | Quote expiry date. Format: "yyyy-MM-dd". |
quote_outcome | Dropdown | Pending, Won, Lost, or Expired. |
drawing_revision | Text | Drawing revision letter or number, e.g. "Rev B" or "3". |
lost_reason | Dropdown | One 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.
| Field | Type | Description |
|---|---|---|
description | string | Item description. |
quantity | number | Number of units. |
unit_price | number | Price per unit in dollars. |
material_cost | number | Material cost for the line (used for margin math). |
markup_percent | number | null | If non-null, unit_price = material_cost × (1 + markup_percent/100). Null = unit price entered directly. |
line_total | number | Computed: quantity × unit_price. |
Material requirements
| Field | Type | Description |
|---|---|---|
material | string | Material name (e.g., "Aluminum 6061-T6"). |
specification | string | Size/spec (e.g., "1/2\" x 4\" x 12\""). |
quantity_needed | number | How much is needed. |
unit | string | One of: pcs, ft, in, m, mm, kg, lb, sheets, bars, tubes, plates, rolls, ea. |
is_allocated | boolean | true if the material has been allocated/consumed. |
notes | string | Free-text. |
Quality notes (NCR records)
Non-conformance reports for ISO 9001 quality tracking. Array; empty for entries with no quality issues.
| Field | Type | Description |
|---|---|---|
created_at | ISO 8601 string | When the NCR was opened. |
created_by | string | Who opened the NCR. |
type | string | One of: Internal, Customer, Supplier. |
description | string | What went wrong. |
disposition | string | One of: Rework, Scrap, Accept As-Is, Return to Supplier, Pending Review. |
root_cause | string | Free-text root-cause analysis (ISO 9001 §10.2.1). |
severity | string | One of: Minor, Major, Critical. |
affected_quantity | integer | Number of non-conforming parts. |
cost_impact | number | Estimated cost impact in dollars. |
corrective_action | string | What was/will be done. |
corrective_action_due_date | string | Format: "yyyy-MM-dd". Empty when no deadline. |
is_resolved | boolean | true when the NCR is closed. |
resolved_at | ISO 8601 string | When closed. Empty if open. |
effectiveness_verified | boolean | ISO 9001 §10.2.1e: did the corrective action actually prevent recurrence? |
effectiveness_verified_at | ISO 8601 string | When verified. |
effectiveness_verified_by | string | Who 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>.
| Field | Type | Description |
|---|---|---|
file_name | string | Original filename as the user uploaded it (e.g., "drawing_v3.pdf"). |
stored_file_name | string | GUID-prefixed name in the attachments folder (e.g., "a1b2c3d4_drawing_v3.pdf"). |
file_size_bytes | integer | File size in bytes. |
category | string | One of: Drawing, Setup Sheet, CNC Program, Photo, Quote, Purchase Order, Inspection Report, Spec Sheet, Other (or empty for uncategorized). |
added_at | ISO 8601 string | When the file was attached. |
added_by | string | Who 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.
| Field | Type | Description |
|---|---|---|
change_count | integer | Number of audit records for this entry. |
last_changed_at | ISO 8601 UTC | null | When the most recent change occurred. Null if never changed. |
last_changed_by | string | Who made the most recent change. Empty if never changed. |
Work state
Live state for shop-floor coordination — hold, rework, and active-work indicators.
| Field | Type | Description |
|---|---|---|
is_on_hold | boolean | The entry is paused waiting on something (material, approval, etc.). |
hold_reason | string | Reason code from the project's configurable list. |
hold_notes | string | Free-text reason notes. |
is_rework | boolean | The entry was sent back from a later stage for rework. |
rework_from_stage | string | Which stage sent it back. |
rework_reason | string | Reason code. |
rework_notes | string | Free-text reason notes. |
rework_returned_by | string | Username of who sent it back. |
rework_returned_at | ISO 8601 string | When it was sent back. |
active_work_by | string | Username of the worker currently on the job. Empty when idle. |
active_work_started_at | ISO 8601 string | When 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.
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
- Get Data → Folder → select your Data Feed folder.
- Filter rows:
Extensionequals".csv"for the CSVs, or".json"forentries.json. - For
entries.json: click "Combine & Transform Data," expand the JSON list, then expand the columns you want. Power BI handles snake_case fields natively. - 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.json → id) 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.