Browse documentation
Tutorial GraphQL

Run your first warehouse query

Obtain a real data-model slug, send a bounded factQuery with variables, and verify empty or populated GraphQL results.

Goal: Query at most ten latest warehouse rows for a real data model.

View as Markdown

Obtain a real data-model slug through JSON:API, then send a bounded GraphQL query. A model with no persisted facts returns an empty results array; that is a valid query but not proof that a workflow has written records.

What you will do

  1. List the visible data models and choose one real slug.
  2. Write a GraphQL query and variables to a temporary JSON request file.
  3. Request at most ten latest rows.
  4. Verify the GraphQL data/error shape and version headers.

Prerequisites

  • Create a model through the Warehouse product flow described in Data models.
  • For a populated result, run a workflow whose harmonizer persists facts for that model.
  • Use a service account in the same workspace with api_read and an active run-as member.
  • Set HODOFLOW_BASE_URL and TOKEN; install cURL and jq.

Do not invent a slug from a display name. The stored slug is the GraphQL table identity.

Request

List the visible data models. This collection currently does not expose a pagination parameter, so it is the documented exception to the portal's explicit-limit rule. Use the response only to select a real slug:

MODELS_RESPONSE="$(
curl --silent --show-error --globoff \
--header "Accept: application/vnd.api+json" \
--header "Authorization: Bearer $TOKEN" \
"$HODOFLOW_BASE_URL/api/json/v1/data_models?fields[data_model]=name,slug"
)"
printf '%s\n' "$MODELS_RESPONSE" | jq '.data[] | {name: .attributes.name, slug: .attributes.slug}'
DATA_MODEL_SLUG="$(printf '%s\n' "$MODELS_RESPONSE" | jq -er '.data[0].attributes.slug')"

Create a request file. Query and variables stay separate, which avoids escaping nested values by hand:

QUERY_FILE="$(mktemp)"
jq -n --arg slug "$DATA_MODEL_SLUG" '{
query: "query FirstFacts($slug: String!, $limit: Int!) { factQuery(dataModelSlug: $slug, mode: \"latest\", limit: $limit) { count hasNextPage results { entityId observedAt data } } }",
variables: {slug: $slug, limit: 10}
}' > "$QUERY_FILE"
curl --silent --show-error --include \
--request POST \
--header "Content-Type: application/json" \
--header "Authorization: Bearer $TOKEN" \
--data-binary "@$QUERY_FILE" \
"$HODOFLOW_BASE_URL/api/graphql"

You should see 200, a date-based x-api-version, x-request-id, and either a data.factQuery object or a GraphQL errors array explaining why resolution did not succeed.

Expected response

A valid model with no matching facts returns:

{
"data": {
"factQuery": {
"count": 0,
"hasNextPage": false,
"results": []
}
}
}

A populated result contains at most ten Fact rows:

{
"data": {
"factQuery": {
"count": 1,
"hasNextPage": false,
"results": [
{
"entityId": "order-123",
"observedAt": "2026-07-31T18:30:00Z",
"data": { "status": "shipped", "total": 125.5 }
}
]
}
}
}

Field names inside data come from your model; they are not a fixed schema in this guide.

Troubleshooting

  • Empty model list: create a data model in the same workspace first.
  • Unknown slug error: re-read .attributes.slug; names and slugs differ.
  • 401: exchange once and retry once.
  • 403: factQuery requires api_read and an active run-as member.
  • Complexity error: reduce limit or selected fields.
  • Empty results: confirm a workflow persisted facts for this exact model and workspace. An empty result is not an authentication failure.
  • 429: obey Retry-After before retrying.

Remove the temporary file and local response values:

rm "$QUERY_FILE"
unset QUERY_FILE MODELS_RESPONSE DATA_MODEL_SLUG

Next steps

Related documentation