OQL · Entities
Deals
OQL field reference for deals. Sales opportunities linked to contacts, tracked through pipeline stages.
Maintained by the OnePageCRM engineering team · Last updated Aug 13, 2026
Deals are sales opportunities linked to contacts and tracked through pipeline stages. The financial fields are aggregatable, which makes deals the natural entity for revenue reporting.
Fields
Legend: F filterable, S sortable, A aggregatable, G groupable.
| Field | Type | F | S | A | G | Description |
|---|---|---|---|---|---|---|
id | ID | Y | — | — | — | Deal ID |
name | string | Y | Y | — | — | Deal name |
text | string | — | — | — | — | Deal description / notes (free text; writable via create/update). Plain text — Markdown and HTML are not rendered. |
contact_id | ID | Y | — | — | Y | Associated contact ID |
owner_id | ID | Y | — | — | Y | Deal owner user ID. Defaults to the calling user on create. |
status | string | Y | Y | — | Y | One of pending, won, lost. Defaults to pending on create. |
amount | number | Y | Y | Y | — | Deal value per recurring period. Equals total_amount when months is 1. |
total_amount | number | Y | Y | Y | — | amount × months. Equals amount when months is 1. Derived — not writable. |
cost | number | Y | Y | Y | — | Deal cost per recurring period, if cost tracking is enabled. Equals total_cost when months is 1. |
total_cost | number | Y | Y | Y | — | cost × months. Equals cost when months is 1. Derived — not writable. |
margin | number | Y | Y | Y | — | Profit margin over all recurring months (total_amount - total_cost). Derived — not writable. |
commission | number | Y | Y | Y | — | Commission as a money value, totalled over all recurring months. Pair with commission_type: "absolute"; read-only when the type is percentage. |
commission_percentage | number | Y | — | Y | — | Commission percentage. Pair with commission_type: "percentage". |
commission_type | string | Y | — | — | Y | How commission is expressed: none, percentage, or absolute. Inferred from whichever value field you send when omitted. |
commission_base | string | Y | — | — | Y | What percentage commission is taken from: amount (meaning total_amount) or margin. Defaults to amount. |
pipeline_id | ID | Y | — | — | Y | Pipeline ID. Defaults to the account’s default pipeline on create. |
stage | number | Y | Y | — | Y | Pipeline stage number. Live for pending deals only. Defaults to the resolved pipeline’s first stage when creating a pending deal. |
last_stage | number (virtual) | Y | Y | — | — | Final stage for closed deals (won, lost, and every delivery-pipeline deal) |
expected_close_date | date | Y | Y | — | Y | Expected close date (when status is pending). Defaults to today on create. |
close_date | date | Y | Y | — | Y | Actual close date (when status is won or lost). Defaults to today when a deal is closed without one. |
months | number | Y | — | — | — | Number of recurring months, 1–100. Defaults to 1. |
reason_lost | string (output only) | — | — | — | — | Reason lost display name |
reason_lost_id | ID | Y | — | — | Y | Reason lost ID |
has_deal_items | boolean | Y | — | — | — | Whether the deal has line items |
archived | boolean | Y | — | — | Y | Whether the deal is archived |
created_at | time | Y | Y | — | Y | Record creation timestamp |
modified_at | time | Y | Y | — | Y | Last modification timestamp |
Notes
stagevslast_stagemap to the same underlying value but are scoped by deal status. Pending deals exposestage; closed deals (won, lost, and every deal in a delivery pipeline) exposelast_stage. The non-applicable one isnullin projections, and filtering bystageimplicitly restricts the result set to pending deals.- Stage values are integers but not sequential. A typical pipeline
uses values like
10,20,40. Stage numbers and their labels are account-configurable. - Sales vs delivery pipelines. A pipeline is either a sales
pipeline (deals run pending → won/lost) or a delivery pipeline
(post-sales project tracking, where every deal is won and the stages
track delivery progress). Deals in a delivery pipeline are always
won, so their position lives inlast_stage, notstage. Usecontext()to see each pipeline’s type. - Creating into a delivery pipeline forces
status: "won". Passing"pending"alongside a deliverypipeline_idis coerced rather than rejected, so check the pipeline’s type incontext()before you choose a status. Pass the position asstageon create either way —last_stageis the field you read it back from, not one you write. - Financial fields (
amount,total_amount,cost,total_cost,margin,commission,commission_percentage) are all aggregatable and valid as the argument tosum,avg,min,max,median, andpercentile. - Recurring deals: per-period vs total.
amountandcostare per-period figures;total_amountandtotal_costmultiply them bymonths.marginis already a total (total_amount - total_cost), notamount - cost, so on a 12-month deal it is 12 times the per-period profit. Non-recurring deals havemonthsof 1, where the per-period and total figures are equal.monthsaccepts 1–100; out-of-range values are clamped, not rejected. The three totals are computed server-side and cannot be written — setamount(andmonthsif it recurs) and they follow.total_amountis the figure the app shows on the deal card, so it is the one to read when the question is “what is this deal worth”. - Commission.
commission_typedecides which field carries the value:percentageusescommission_percentage,absoluteusescommissionas a money value,nonemeans no commission. Omit the type and it is inferred from whichever of the two you send. Underpercentagethe server computescommissionand it becomes read-only.commission_basepicks what the percentage is taken from —amount(which meanstotal_amount, across all recurring months, not the per-period figure) ormargin— and defaults toamount. It is ignored when the type isnone. - No currency is attached. Every money field is a bare number. The
account’s currency and its symbol are in
context()asaccount.currencyandaccount.currency_symbol. Read them before rendering a money value — the number alone gives no indication of the unit. reason_lostis a display alias, resolved fromreason_lost_id. Selecting it is rejected outright — selectreason_lost_idand map it throughcontext()’sreasons_lostlist for the label.close_dateis writable via the MCPcreate/updatetools, but only on won or lost deals (or together withstatus: "won"/"lost"in the same call). Pending deals have no close date; setting it on one is rejected with a hint to useexpected_close_dateinstead.- Lookups:
contact(viacontact_id). Pull the contact’s fields inline ascontact.<field>— for examplecontact.last_name,contact.company_name, orcontact.country_code— inselect,where, andorder_by. See Concepts › Lookups. - Custom fields are queryable by name as
custom_fields.<name>inselect,where, andorder_by; selectcustom_fields.*(or*) to return all of them. Filterability and sortability depend on the field’s type. Numeric custom fields can also be aggregated ({ "sum": ["custom_fields.<name>"] }), andmin/maxwork on date custom fields.group_byanddistincton custom fields are not supported: group by a top-level field and filter on the custom field inwhereinstead. Usedescribeto discover an account’s custom-field names and types.
Example queries
Top 25 open deals by amount, each with its contact’s company (a lookup):
{
"from": "deals",
"select": ["name", "amount", "contact.company_name"],
"where": { "status": "pending" },
"order_by": [{ "amount": "desc" }],
"limit": 25
}
Deals I own that closed last quarter:
{
"from": "deals",
"where": {
"owner_id": "ME()",
"status": "won",
"close_date": "LAST_QUARTER()"
}
}
Won revenue this quarter, by owner:
{
"from": "deals",
"select": ["owner_id", "count()", { "sum": ["amount"] }],
"where": { "status": "won", "close_date": "THIS_QUARTER()" },
"group_by": ["owner_id"]
}
Monthly closed-won trend over the last year:
{
"from": "deals",
"select": ["close_date", "count()", { "sum": ["amount"] }],
"where": {
"status": "won",
"close_date": { ">=": { "DAYS_AGO": [365] } }
},
"group_by": [{ "MONTH": ["close_date"] }]
}
Stuck deals (in pipeline more than 90 days):
{
"from": "deals",
"where": {
"status": "pending",
"created_at": { "<": { "DAYS_AGO": [90] } }
},
"order_by": [{ "amount": "desc" }]
}