graphistry/graphistry-skills

pygraphistry-gfql

Construct and run GFQL graph queries in PyGraphistry using chain-list syntax or Cypher strings. Use when asked to "query my graph with GFQL", "MATCH pattern in graphistry", "find paths between nodes", "hop constraints", "let bindings", "GRAPH constructor", or "run Cypher on my graph". Also triggers on "g.gfql()", "n() e_forward() n()", "chain-list query", "subgraph extraction", "remote graph query", or "pattern matching in graphistry". Proactively suggest when the user wants multi-hop traversal…

First seen Mar 3, 2026

Installation

$ npx skills add graphistry/graphistry-skills --skill pygraphistry-gfql

Also in this package

Other skills from graphistry/graphistry-skills · top by installs.

npx skills add graphistry/graphistry-skills

Browse all from graphistry/graphistry-skills

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Repository health

Stars 2
License LICENSE
Default branch main
Open issues 8
Status Active

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 15,127 B
  • docs SUMMARY.md 587 B

History

  1. First seen on skills.sh
  2. First recorded snapshot · 35 installs

SKILL.md

PyGraphistry GFQL

Doc routing (local + canonical)

  • First route with ../pygraphistry/references/pygraphistry-readthedocs-toc.md.
  • Use ../pygraphistry/references/pygraphistry-readthedocs-top-level.tsv for section-level shortcuts.
  • Only scan ../pygraphistry/references/pygraphistry-readthedocs-sitemap.xml when a needed page is missing.
  • Use one batched discovery read before deep-page reads; avoid cat * and serial micro-reads.
  • In user-facing answers, prefer canonical https://pygraphistry.readthedocs.io/en/latest/... links.

Two syntaxes, one entrypoint

g.gfql() accepts both chain-list (Python AST objects) and Cypher strings. It auto-detects the language from the argument type:

# Chain-list syntax (Python AST objects)
g2 = g.gfql([n({'type': 'person'}), e_forward(), n()])

# Cypher string syntax (auto-detected)
g2 = g.gfql("MATCH (p:Person)-[r:KNOWS]->(q:Person) RETURN p.name, q.name")

# Explicit language parameter (optional)
g2 = g.gfql(query_string, language="cypher")

When to use which:

  • Chain-list: Programmatic composition, dynamic parameterization, when building queries from code
  • Cypher: Readability, familiarity for Cypher users, complex pattern matching with RETURN/ORDER BY/LIMIT

Quick start — chain-list

from graphistry import n, e_forward

g2 = g.gfql([
    n({'type': 'person'}),
    e_forward({'relation': 'transfers_to'}, min_hops=1, max_hops=3),
    n({'risk': True})
])

Quick start — Cypher

# Simple pattern match
g2 = g.gfql("MATCH (p:Person)-[r:KNOWS]->(q:Person) WHERE p.age > 30 RETURN p.name, q.name")

# Variable-length paths
g2 = g.gfql("MATCH (a:Account)-[*1..3]->(m:Merchant) RETURN a, m")

# Parameterized queries
g2 = g.gfql(
    "MATCH (n) WHERE n.score > $cutoff RETURN n.id, n.score ORDER BY n.score DESC LIMIT $top_n",
    params={"cutoff": 50, "top_n": 10}
)

# Relationship type alternation
g2 = g.gfql("MATCH (a:Person)-[:KNOWS|COLLABORATES_WITH]->(b:Person) RETURN a.name, b.name")

See references/gfql-cypher.md for the full clause/function inventory and label-column mapping.

GRAPH constructor and Let/DAG bindings

from graphistry import n, e_forward, let, ref

# Subgraph as a graph object (not a table)
subgraph = g.gfql("GRAPH { MATCH (a)-[r]->(b) WHERE a.risk_score > 7 }")

# Named bindings forming a DAG; ref() operates on the referenced binding's output
result = g.gfql(let({
    'high_risk': n({'risk_score': {'$gt': 0.8}}),
    'neighborhoods': ref('high_risk', [e_forward(max_hops=2), n()])
}), output='neighborhoods')

Independent bindings operate on the root graph. See references/gfql-cypher.md for multi-stage DAGs, nested let scope rules, and the GRAPH/USE pipeline form.

Targeted patterns (high signal)

# Edge query filtering
g2 = g.gfql([n(), e_forward(edge_query="type == 'replied_to' and submolt == 'X'"), n()])
# Same-path constraints with where + compare/col
from graphistry import col, compare
g2 = g.gfql([n(name='a'), e_forward(name='e'), n(name='b')], where=[compare(col('a', 'owner_id'), '==', col('b', 'owner_id'))])
# Traverse 2-4 hops but only return hops 3-4
g2 = g.gfql([e_forward(min_hops=2, max_hops=4, output_min_hops=3, output_max_hops=4)])

Edge direction variants

  • e_forward() — source-to-destination
  • e_reverse() — destination-to-source
  • e_undirected() — both directions
  • e() — alias for any direction

High-value patterns

  • g.gfql() is the unified entrypoint — pass chain-lists OR Cypher strings.
  • NEVER use .chain() or .hop() — they are deprecated and emit warnings. Always use g.gfql([...]) for chain-list syntax or g.gfql("MATCH ...") for Cypher.
  • When user explicitly asks for GFQL, final snippets must include explicit .gfql(...).
  • When the task says remote execution/dataset, use gfql_remote(...).
  • Use name= labels for intermediate matches when you need constraints.
  • Use where=[...] for cross-step/path constraints.
  • Use minhops/maxhops and outputminhops/outputmaxhops for traversal vs returned slice.
  • Use predicates (is_in, numeric/date predicates) for concise filtering.
  • Use an explicit engine when performance or result frame type matters; engine='auto' does not select Polars.

Execution engines: pandas, Polars, cuDF, and Polars-GPU

Use the same query with the engine suited to the workload. Input frame type and execution engine are independent; GFQL converts inputs once and returns frames in the selected engine's type.

query = "MATCH (a)-[e]->(b) WHERE a.risk_score > $cutoff RETURN b"

cpu_out = g.gfql(query, params={'cutoff': 7}, engine='polars')
gpu_out = g.gfql(query, params={'cutoff': 7}, engine='polars-gpu')

# Polars/Polars-GPU outputs are polars.DataFrame objects
nodes_pd = cpu_out._nodes.to_pandas()  # only for pandas-only downstream APIs
  • Valid literals are exactly 'pandas', 'cudf', 'dask', 'dask_cudf', 'polars', 'polars-gpu',

'auto'. 'polars-gpu' is hyphenated; polars_gpu is not an engine.

  • For a Polars input graph engine='auto' resolves to pandas — pass engine='polars' to stay native.
  • Outputs follow the selected engine: Polars for polars/polars-gpu, cuDF for cudf. Convert

intentionally before pandas-only operations such as .iloc or groupby().apply().

  • polars-gpu requires the RAPIDS stack and is GPU-or-error — it never silently falls back to CPU.

Analytics under Polars engines

Whole-graph call() analytics such as UMAP, hypergraph, layouts, or computecugraph are not native Polars operations. With the default callmode='auto', GFQL bridges them off-engine (pandas for Polars; cuDF for Polars-GPU) and converts the result back. Use strict mode when an off-engine bridge would violate a benchmark, memory, or execution constraint:

from graphistry.compute.gfql.lazy import set_call_mode

set_call_mode('strict')  # reject an off-engine analytic before it runs
try:
    result = g.gfql(query, engine='polars')
except NotImplementedError as exc:
    ...  # strict mode declined an off-engine analytic

gfql() takes no strict= or callmode= argument. Mode is process-level via setcallmode('auto'|'strict') or GFQLPOLARSCALLMODE (Python override > env > default 'auto'), read live per call. Strict raises NotImplementedError — not RuntimeError, not a warning. Because it is process-global, scope it and restore in a finally: when only one step must be strict (references/gfql-engines.md).

polars-gpu analytics are GPU-or-error: if the GPU/cuDF stack is unavailable, they decline rather than move the work to host pandas.

Engine tuning knobs

Three process-level settings live in graphistry.compute.gfql.lazy, each resolving Python override > env var > default and read live per collect (not frozen at import):

setting values default env var
setcallmode 'auto', 'strict' 'auto' GFQLPOLARSCALL_MODE
setgpuexecutor 'in-memory', 'streaming' 'in-memory' GFQLPOLARSGPU_EXECUTOR
setcpustreaming True, False False GFQLPOLARSCPU_STREAMING

Read the current value with callmode(), gpuexecutor(), cpu_streaming(); pass None to a setter to reset to env/default. 'polars' and 'polars-gpu' are one lazy engine with two collect targets, so the plan is built once and collected once. That transfer-once design is what makes GPU pay off:

  • Collect-once avoids repeated host-to-device (H2D) transfers. Per-op eager collection re-copies the

frame to the device on every operation, which benchmarked as a GPU regression.

  • The knobs resolve Python override > env var > default, read live per collect — not frozen at import.

Physical indexes: seeded lookups

GFQL ships pay-as-you-go adjacency/node-id indexes (graphistry.compute.gfql.index) for seeded traversal. Two rules decide whether an index helps:

  • Query shape — only the chain/hop form consults the index. Measured: a seeded chain went

8.90ms → 1.68ms (5.3x), while both Cypher spellings were never consulted and did not improve. Write the chain form if you want index acceleration.

  • Frontier size, engine-aware. The planner gates index-vs-scan on the frontier as a fraction of

distinct source keys: pandas ~0.5, polars/cuDF/GPU ~0.02. Vectorized engines scan fast enough that an index only wins for very selective seeds. Past the gate it falls back to scan, so it never loses.

from graphistry.compute.gfql.index import create_index, index_trace
gi = create_index(g, 'edge_out_adj', engine='polars')   # edge_out_adj | edge_in_adj | node_id
with index_trace() as steps:
    out = gi.gfql([n({'id': 'acct-42'}), e_forward(), n()], engine='polars')
steps[0]['path']   # confirm 'index', do not assume it

index_policy= is a per-call gfql() keyword (unlike call mode): 'use' (default, resident + cost-gated), 'auto' (build on demand), 'force' (skip the gate), 'off'. With no resident index, 'use' silently scans. Index DDL, trace fields, and the cost-gate override: references/gfql-engines.md.

Choosing an engine on performance

Do not promise a speedup you have not measured.

  • pandas → polars: worth it above roughly 50–100k rows; below that conversion can dominate. Measured ~2x on seeded 1-hop.
  • polars → polars-gpu is not a blanket win. Measured on an NVIDIA GB10: **0.83x at 100k rows (slower

than CPU), 1.41x at 1M, 0.98x at 5M** — a band, not a rising curve. At 8M edges cudf beat both. Never state a general "polars-gpu beats cudf" rule.

  • setcpustreaming(True) is opt-in and can be slower — ~0.86x on small/interactive sizes. Large

batch CPU work only; the name sounds faster than it is.

  • Full measurement tables: references/gfql-engines.md.

Which engine when — a decision procedure

Work in order; stop at the first that decides.

  1. Does a step decline under Polars? Run that step on engine='pandas' and report pandas as the engine.
  2. Where do the frames already live? polars-gpu ingests a host polars frame, so already-on-device

cuDF frames favor engine='cudf'; host Polars frames favor 'polars'/'polars-gpu'.

  1. Seeded lookup with a small frontier? Index it and use the chain form — the biggest single win

available (5.3x), independent of engine choice.

  1. CPU: prefer polars over pandas for anything non-trivial.
  2. GPU: only when the workload is big enough, and pick the GPU engine by measurement, not by name.

Below a few milliseconds of work, engine choice is noise — indexing and query shape matter more.

Parity-or-decline: do not invent workarounds

Refuse to mislabel first, then solve the problem. If a user asks you to keep reporting engine='polars' for work that pandas executed — to keep a dashboard green, a benchmark comparable, or an API contract stable — say no before writing any code. A wrapper that exposes engine='polars' while pandas runs underneath is mislabeling even when a second field records the truth: the primary label is the one people read. This is the one request in this skill you should push back on rather than implement.

Honest alternatives to offer: run the step on engine='pandas' and report pandas, or keep the pipeline polars-native by avoiding the declining surface.

With that settled, the mechanics: traversal, filter, and row ops under a Polars engine are parity-or-NotImplementedError.

  • The engine never silently falls back to pandas — a hidden bridge would misreport pandas performance as Polars.
  • An unsupported surface raises NotImplementedError (not RuntimeError, not a warning).
  • Surfaces that decline today: undirected minhops>1, direct hop(minhops>1) (use chain()/gfql()),

multi-entity rows(binding_ops=…), cross-entity same-path WHERE, exotic expressions (CASE/list/map/temporal). Full list: references/gfql-engines.md.

Conversion into an engine follows the repo-wide validate/warn convention. On a mixed-type object column that Arrow cannot represent:

validate behavior
'strict' raises an error (NotImplementedError for polars); the column is left unchanged
'autofix' coerces the column to string and emits a warning — data is silently rewritten unless you read the warning

Use 'strict' for any job that must never change data without telling you.

Remote mode

# Remote with chain-list
rg = graphistry.bind(dataset_id='my-dataset')
res = rg.gfql_remote([n(), e_forward(), n()], engine='auto')
# Remote with Cypher string
res = rg.gfql_remote("MATCH (n:Person)-[r]->(m) WHERE n.risk_level = 'critical' RETURN n, r, m")
# Remote with Let/DAG
res = rg.gfql_remote(let({...}))
# Remote slim payload (only required columns)
res = rg.gfql_remote([n(), e_forward(), n()], output_type='nodes', node_col_subset=['node_id', 'time'])
# Post-process on remote side when you want trimmed transfer payloads
res = rg.python_remote_table(lambda g: g._edges[['src', 'dst']].head(1000))

Validation and safety

  • Validate user-derived query fragments before execution.
  • Normalize datetime columns before temporal predicates.
  • Prefer small column subsets for remote result transfer.
  • Preflight Cypher: from graphistry.compute.gfql.cypher import parsecypher, compilecypher

Canonical docs