You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tinkerpop.apache.org by sp...@apache.org on 2026/06/30 19:16:10 UTC

(tinkerpop) branch jsprocess created (now 84586ac667)

This is an automated email from the ASF dual-hosted git repository.

spmallette pushed a change to branch jsprocess
in repository https://gitbox.apache.org/repos/asf/tinkerpop.git


      at 84586ac667 Add is() and loops() to gremlin-javascript Tiny Gremlin

This branch includes the following new commits:

     new fb51129928 Add Tiny Gremlin — local in-process traversal for gremlin-javascript
     new c7392acd0f Add by() modulation to path() in gremlin-javascript Tiny Gremlin
     new eb339391b4 Document Tiny Gremlin in the gremlin-javascript reference
     new 9aa6339230 Minor docs improvements for Tiny Gremlin
     new c35e817ad6 Add repeat() to gremlin-javascript Tiny Gremlin
     new 84586ac667 Add is() and loops() to gremlin-javascript Tiny Gremlin

The 6 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



(tinkerpop) 02/06: Add by() modulation to path() in gremlin-javascript Tiny Gremlin

Posted by sp...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

spmallette pushed a commit to branch jsprocess
in repository https://gitbox.apache.org/repos/asf/tinkerpop.git

commit c7392acd0ff0c87de250bb40f97895f71c2cac81
Author: Stephen Mallette <st...@amazon.com>
AuthorDate: Fri Jun 26 15:38:43 2026 -0400

    Add by() modulation to path() in gremlin-javascript Tiny Gremlin
    
    path() now projects each path element via by(). Supported forms: by(String),
    by(T), and by(Traversal) restricted to a single value-extraction step (id,
    label, key, value, values, valueMap, elementMap; values takes the first item).
    Multiple by() modulators apply round-robin across path elements. A non-productive
    by() filters the path traverser, matching default Gremlin semantics
    (ProductiveByStrategy is not supported).
    
    - LocalExecutor: fold consecutive by() into path(); applyPath runs the single
      value-extraction sub-step per element; reject by(Traversal) on order()
    - steps.ts: round-robin projection in stepPath with NON_PRODUCTIVE filtering
    - ExecuteVisitor: parse the by(Traversal) sub-pipeline (was a discarded placeholder)
    - local-graph-connection: drop the erroneous override on commit()/rollback(), which
      are not part of the RemoteConnection abstract contract, so the test suite compiles
    
    Tests:
    - Path.feature: tag three existing path().by() scenarios @TinyGremlin and add
      by(T.label) and by(__.values("name")) scenarios
    - new unit test for by(T), by(Traversal) and the single-step restriction error
    - new integration test exercising the primary use case: subgraph() from a remote
      graph then local Tiny Gremlin traversal, plus JavaScript closure post-processing
      (map/filter/flat/reduce/forEach) for steps outside the subset
    
    Regenerates the cross-language feature test code from the corpus, which also catches
    up generated code left stale by the prior Tiny Gremlin commit (AddVertex scenarios).
    
    Assisted-by: Claude Code:claude-opus-4-8
---
 docs/src/dev/provider/tiny-gremlin.asciidoc        |  16 +-
 .../Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs |   4 +
 gremlin-go/driver/cucumber/gremlin.go              |   4 +
 .../lib/driver/local-graph-connection.ts           |   8 +-
 .../lib/language/executor/ExecuteVisitor.ts        |  12 +-
 .../lib/process/local/LocalExecutor.ts             |  58 +++++-
 .../gremlin-javascript/lib/process/local/steps.ts  |  44 ++++-
 .../gremlin-javascript/test/cucumber/gremlin.js    |   6 +-
 .../integration/tiny-gremlin-subgraph-tests.js     | 218 +++++++++++++++++++++
 .../test/unit/tiny-gremlin-path-by-test.js         |  75 +++++++
 .../src/main/python/tests/feature/gremlin.py       |   4 +
 .../gremlin/language/translator/translations.json  | 128 ++++++++++++
 .../gremlin/test/features/map/Path.feature         |  36 +++-
 13 files changed, 598 insertions(+), 15 deletions(-)

diff --git a/docs/src/dev/provider/tiny-gremlin.asciidoc b/docs/src/dev/provider/tiny-gremlin.asciidoc
index 2dd151598a..bffd26056e 100644
--- a/docs/src/dev/provider/tiny-gremlin.asciidoc
+++ b/docs/src/dev/provider/tiny-gremlin.asciidoc
@@ -304,9 +304,23 @@ Maps an edge to its outgoing vertex (the tail of the arrow).
 
 Maps the current traverser to the `Path` of elements it has visited.
 
+`by()` modulation is supported to project each element in the path. Multiple `by()` modulators are applied
+round-robin across the path elements (the first element uses the first `by()`, the second the second, and so on,
+cycling back to the first). The following forms are supported:
+
+* `by(String)` — project the named property value (the first value for multi-properties).
+* `by(T)` — project `T.id` or `T.label`.
+* `by()` — natural identity (the raw element), equivalent to omitting `by()` for that position.
+* `by(Traversal)` — restricted to a *single value-extraction step*: `id()`, `label()`, `key()`, `value()`,
+  `values()`, `valueMap()`, or `elementMap()`. `values()` projects the first value. Any other traversal shape
+  raises an error.
+
+A non-productive `by()` (for example, `by("age")` on an element without an `age` property) filters the whole
+path traverser, matching default Gremlin semantics. `ProductiveByStrategy` is not supported, so there is no
+null-producing variant.
+
 *Tiny Gremlin limitations:*
 
-* `by()` modulation is not supported. Path objects always contain the raw elements visited.
 * `from()` and `to()` selection within a path is not supported because `as()` step labels are excluded from Tiny
   Gremlin.
 
diff --git a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs
index 0d610f47d9..8424c6aba5 100644
--- a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs
+++ b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs
@@ -1033,6 +1033,8 @@ namespace Gremlin.Net.IntegrationTest.Gherkin
                {"g_addV_propertyXset_emptyX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.AddV((string) "foo").Property(Cardinality.Set, new Dictionary<object, object> {}), (g,p) =>g.V().HasLabel("person").Values<object>()}}, 
                {"g_addVXpersonX_propertyXname_joshX_propertyXage_nullX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.AddV((string) "person").Property("name", "josh").Property("age", null), (g,p) =>g.V().Has("person", "age", (object) null)}}, 
                {"g_addVXpersonX_propertyXname_markoX_propertyXfriendWeight_null_acl_nullX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.AddV((string) "person").Property("name", "marko").Property("friendWeight", null, "acl", null), (g,p) =>g.V().Has("person", "name", "marko").Has("friendWeight", (object) null), (g,p) =>g.V().Has("person", "name", "marko").Properties<object>("friendWeight").Has("acl", (object) null), (g,p) =>g.V().Has("person",  [...]
+               {"g_V_hasXperson_name_aliceX_propertyXage_51X", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.AddV((string) "person").Property("name", "alice").Property("age", 50), (g,p) =>g.V().Has("person", "name", "alice").Property("age", 51), (g,p) =>g.V().Has("person", "age", 50), (g,p) =>g.V().Has("person", "age", 51)}}, 
+               {"g_addVXpersonX_propertyXname_aliceX_propertyXage_30X_query", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.AddV((string) "person").Property("name", "alice").Property("age", 30), (g,p) =>g.V().Has("person", "name", "alice"), (g,p) =>g.V().Has("person", "age", 30)}}, 
                {"g_V_hasXperson_name_aliceX_propertyXsingle_age_unionXage_constantX1XX_sumX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.AddV((string) "person").Property("name", "alice").Property(Cardinality.Single, "age", 50), (g,p) =>g.V().Has("person", "name", "alice").Property("age", __.Union<object>(__.Values<object>("age"), __.Constant<object>(1)).Sum<object>()), (g,p) =>g.V().Has("person", "age", 50), (g,p) =>g.V().Has("person", "age", [...]
                {"g_V_limitX3X_addVXsoftwareX_aggregateXa1X_byXlabelX_aggregateXa2X_byXlabelX_capXa1_a2X_selectXa_bX_byXunfoldX_foldX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.AddV((string) "person").Property("name", "marko").Property("age", 29).As("marko").AddV((string) "person").Property("name", "vadas").Property("age", 27).As("vadas").AddV((string) "software").Property("name", "lop").Property("lang", "java").As("lop").AddV((string) "pers [...]
                {"g_addV_propertyXname_markoX_withXkey_valueX_valuesXname_keyX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.AddV().Property("name", "marko").With("key", "value").Values<object>("name", "key")}}, 
@@ -1666,6 +1668,8 @@ namespace Gremlin.Net.IntegrationTest.Gherkin
                {"g_VX1X_outEXcreatedX_inV_inE_outV_path", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.V(p["vid1"]).OutE("created").InV().InE().OutV().Path()}}, 
                {"g_V_asXaX_out_asXbX_out_asXcX_path_fromXbX_toXcX_byXnameX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.V().As("a").Out().As("b").Out().As("c").Path().From("b").To("c").By("name")}}, 
                {"g_VX1X_out_path_byXageX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.V(p["vid1"]).Out().Path().By("age")}}, 
+               {"g_VX1X_out_path_byXlabelX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.V(p["vid1"]).Out().Path().By(T.Label)}}, 
+               {"g_VX1X_out_path_byXvaluesXnameXX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.V(p["vid1"]).Out().Path().By(__.Values<object>("name"))}}, 
                {"g_withStrategiesXProductiveByStrategyX_VX1X_out_path_byXageX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.WithStrategies(new ProductiveByStrategy()).V(p["vid1"]).Out().Path().By("age")}}, 
                {"g_injectX1_null_nullX_path", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.Inject<object>(1, null, null).Path()}}, 
                {"g_injectX1_null_nullX_path_dedup", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.Inject<object>(1, null, null).Path().Dedup()}}, 
diff --git a/gremlin-go/driver/cucumber/gremlin.go b/gremlin-go/driver/cucumber/gremlin.go
index 70822b8eba..253337d7c5 100644
--- a/gremlin-go/driver/cucumber/gremlin.go
+++ b/gremlin-go/driver/cucumber/gremlin.go
@@ -1003,6 +1003,8 @@ var translationMap = map[string][]func(g *gremlingo.GraphTraversalSource, p map[
     "g_addV_propertyXset_emptyX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.AddV("foo").Property(gremlingo.Cardinality.Set, map[interface{}]interface{}{ })}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().HasLabel("person").Values()}}, 
     "g_addVXpersonX_propertyXname_joshX_propertyXage_nullX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.AddV("person").Property("name", "josh").Property("age", nil)}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Has("person", "age", nil)}}, 
     "g_addVXpersonX_propertyXname_markoX_propertyXfriendWeight_null_acl_nullX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.AddV("person").Property("name", "marko").Property("friendWeight", nil, "acl", nil)}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Has("person", "name", "marko").Has("friendWeight", nil)}, func(g *gremlingo.GraphTraversalSource, p map[string]interfa [...]
+    "g_V_hasXperson_name_aliceX_propertyXage_51X": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.AddV("person").Property("name", "alice").Property("age", 50)}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Has("person", "name", "alice").Property("age", 51)}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Has("perso [...]
+    "g_addVXpersonX_propertyXname_aliceX_propertyXage_30X_query": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.AddV("person").Property("name", "alice").Property("age", 30)}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Has("person", "name", "alice")}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Has("person", " [...]
     "g_V_hasXperson_name_aliceX_propertyXsingle_age_unionXage_constantX1XX_sumX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.AddV("person").Property("name", "alice").Property(gremlingo.Cardinality.Single, "age", 50)}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Has("person", "name", "alice").Property("age", gremlingo.T__.Union(gremlingo.T__.Values("age"), gremlingo.T_ [...]
     "g_V_limitX3X_addVXsoftwareX_aggregateXa1X_byXlabelX_aggregateXa2X_byXlabelX_capXa1_a2X_selectXa_bX_byXunfoldX_foldX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.AddV("person").Property("name", "marko").Property("age", 29).As("marko").AddV("person").Property("name", "vadas").Property("age", 27).As("vadas").AddV("software").Property("name", "lop").Property("lang", "java").As("lop").AddV("person").Property("name", "josh").Prop [...]
     "g_addV_propertyXname_markoX_withXkey_valueX_valuesXname_keyX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.AddV().Property("name", "marko").With("key", "value").Values("name", "key")}}, 
@@ -1636,6 +1638,8 @@ var translationMap = map[string][]func(g *gremlingo.GraphTraversalSource, p map[
     "g_VX1X_outEXcreatedX_inV_inE_outV_path": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V(p["vid1"]).OutE("created").InV().InE().OutV().Path()}}, 
     "g_V_asXaX_out_asXbX_out_asXcX_path_fromXbX_toXcX_byXnameX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().As("a").Out().As("b").Out().As("c").Path().From("b").To("c").By("name")}}, 
     "g_VX1X_out_path_byXageX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V(p["vid1"]).Out().Path().By("age")}}, 
+    "g_VX1X_out_path_byXlabelX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V(p["vid1"]).Out().Path().By(gremlingo.T.Label)}}, 
+    "g_VX1X_out_path_byXvaluesXnameXX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V(p["vid1"]).Out().Path().By(gremlingo.T__.Values("name"))}}, 
     "g_withStrategiesXProductiveByStrategyX_VX1X_out_path_byXageX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.WithStrategies(gremlingo.ProductiveByStrategy()).V(p["vid1"]).Out().Path().By("age")}}, 
     "g_injectX1_null_nullX_path": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Inject(1, nil, nil).Path()}}, 
     "g_injectX1_null_nullX_path_dedup": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Inject(1, nil, nil).Path().Dedup()}}, 
diff --git a/gremlin-js/gremlin-javascript/lib/driver/local-graph-connection.ts b/gremlin-js/gremlin-javascript/lib/driver/local-graph-connection.ts
index 0828caf8f9..2601e4eff6 100644
--- a/gremlin-js/gremlin-javascript/lib/driver/local-graph-connection.ts
+++ b/gremlin-js/gremlin-javascript/lib/driver/local-graph-connection.ts
@@ -78,11 +78,13 @@ export class LocalGraphConnection extends RemoteConnection {
     return new RemoteTraversal(generator);
   }
 
-  override async commit(): Promise<void> {
-    // no-op: local graph mutations are in-memory only
+  // commit()/rollback() are not part of the RemoteConnection abstract contract, so they
+  // are plain methods (no 'override'); local graph mutations are in-memory only.
+  async commit(): Promise<void> {
+    // no-op
   }
 
-  override async rollback(): Promise<void> {
+  async rollback(): Promise<void> {
     // no-op
   }
 
diff --git a/gremlin-js/gremlin-javascript/lib/language/executor/ExecuteVisitor.ts b/gremlin-js/gremlin-javascript/lib/language/executor/ExecuteVisitor.ts
index 99957284f2..37c5808196 100644
--- a/gremlin-js/gremlin-javascript/lib/language/executor/ExecuteVisitor.ts
+++ b/gremlin-js/gremlin-javascript/lib/language/executor/ExecuteVisitor.ts
@@ -449,8 +449,16 @@ export class ExecuteVisitor {
     this.push('by', [null, dir]);
   }
 
-  visitTraversalMethod_by_Traversal(_ctx: any): void { this.push('by(Traversal)', []); }
-  visitTraversalMethod_by_Traversal_Comparator(_ctx: any): void { this.push('by(Traversal)', []); }
+  // by(Traversal) — emit the parsed sub-pipeline; the local executor restricts it to a
+  // single value-extraction step (e.g. elementMap()) when used as a path() projection.
+  visitTraversalMethod_by_Traversal(ctx: any): void {
+    this.push('by', [this.visitNestedTraversalAsSubPipeline(ctx.nestedTraversal())]);
+  }
+
+  // by(Traversal, Comparator) — Order is irrelevant for path() projection and is dropped.
+  visitTraversalMethod_by_Traversal_Comparator(ctx: any): void {
+    this.push('by', [this.visitNestedTraversalAsSubPipeline(ctx.nestedTraversal())]);
+  }
 
   /** by(T, Order) — T.id, T.label etc. with a direction, routed through traversalFunction */
   visitTraversalMethod_by_Function(ctx: any): void {
diff --git a/gremlin-js/gremlin-javascript/lib/process/local/LocalExecutor.ts b/gremlin-js/gremlin-javascript/lib/process/local/LocalExecutor.ts
index ee4c04dc63..059b1ddc17 100644
--- a/gremlin-js/gremlin-javascript/lib/process/local/LocalExecutor.ts
+++ b/gremlin-js/gremlin-javascript/lib/process/local/LocalExecutor.ts
@@ -22,7 +22,7 @@ import { defaultPasses, type OptimizationPass } from './passes.js';
 import { LocalExecutionError } from './LocalExecutionError.js';
 import { Graph, Vertex, Edge } from '../../structure/graph.js';
 import {
-  wrap, getValue, type StreamItem,
+  wrap, getValue, NON_PRODUCTIVE, type StreamItem,
   stepOut, stepIn, stepBoth, stepOutE, stepInE, stepBothE, stepOutV, stepInV, stepOtherV,
   stepHas, stepHasId, stepHasLabel, stepHasNot,
   stepId, stepLabel, stepValue, stepKey, stepValues, stepValueMap, stepElementMap,
@@ -33,6 +33,11 @@ import {
 
 type StepFn = (source: Iterable<StreamItem>, args: Arg[], graph: Graph, trackPaths: boolean) => Generator<StreamItem>;
 
+/** Steps allowed inside a by(Traversal) projection on path() — single value extraction only. */
+const VALUE_EXTRACTION_STEPS = new Set<string>([
+  'id', 'label', 'key', 'value', 'values', 'valueMap', 'elementMap',
+]);
+
 /**
  * Executes a Tiny Gremlin Pipeline against a local in-memory Graph, returning
  * an AsyncGenerator that yields one result element at a time.
@@ -76,12 +81,37 @@ export class LocalExecutor {
         // Fold a single following by() modulator into the order step args
         const next = pipeline[i + 1];
         if (next?.name === 'by') {
+          if (Array.isArray(next.args[0])) {
+            throw new LocalExecutionError('by(Traversal) is not supported for order() in Tiny Gremlin');
+          }
           result.push({ name: 'order', args: next.args }); // args = [key, direction]
           i += 2;
         } else {
           result.push(step);
           i++;
         }
+      } else if (step.name === 'path') {
+        // Fold all following by() modulators into the path step as a list of projections.
+        // Each projection is a key (String/T/null) or a single-step value-extraction
+        // sub-pipeline (by(Traversal)); they are applied round-robin across path elements.
+        const projections: Arg[] = [];
+        let j = i + 1;
+        while (j < pipeline.length && pipeline[j].name === 'by') {
+          const proj = pipeline[j].args[0];
+          if (Array.isArray(proj)) {
+            const sub = proj as unknown as Pipeline;
+            if (sub.length !== 1 || !VALUE_EXTRACTION_STEPS.has(sub[0].name)) {
+              throw new LocalExecutionError(
+                'Tiny Gremlin only supports by(Traversal) on path() with a single value-extraction ' +
+                'step (id, label, key, value, values, valueMap, elementMap)',
+              );
+            }
+          }
+          projections.push(proj);
+          j++;
+        }
+        result.push({ name: 'path', args: projections });
+        i = j;
       } else {
         result.push(step);
         i++;
@@ -115,11 +145,35 @@ export class LocalExecutor {
 
   private applyStep(step: StepDescriptor, source: Iterable<StreamItem>, graph: Graph, trackPaths: boolean): Iterable<StreamItem> {
     if (step.name === 'addE') return this.applyAddE(source, step, graph, trackPaths);
+    if (step.name === 'path') return this.applyPath(source, step, graph, trackPaths);
     const fn = STEP_REGISTRY.get(step.name);
     if (!fn) throw new LocalExecutionError(`No implementation for step '${step.name}'`);
     return fn(source, step.args, graph, trackPaths);
   }
 
+  /**
+   * Executes path() with its folded by() projections. Provides stepPath a callback that
+   * runs a single value-extraction sub-step against one path element, taking its first
+   * result (e.g. values('name') yields the first value).
+   */
+  private applyPath(
+    source: Iterable<StreamItem>,
+    step: StepDescriptor,
+    graph: Graph,
+    trackPaths: boolean,
+  ): Iterable<StreamItem> {
+    const runTraversal = (sub: Pipeline, object: any): any => {
+      const sub0 = sub[0];
+      const fn = STEP_REGISTRY.get(sub0.name);
+      if (!fn) throw new LocalExecutionError(`No implementation for step '${sub0.name}'`);
+      for (const out of fn([wrap(object, [], false)], sub0.args, graph, false)) {
+        return getValue(out, false);
+      }
+      return NON_PRODUCTIVE; // non-productive by(Traversal) filters the path traverser
+    };
+    return stepPath(source, step.args, trackPaths, runTraversal);
+  }
+
   private applyAddE(
     source: Iterable<StreamItem>,
     step: StepDescriptor,
@@ -237,7 +291,7 @@ const STEP_REGISTRY = new Map<string, StepFn>([
   ['values',      stepValues],
   ['valueMap',    stepValueMap],
   ['elementMap',  stepElementMap],
-  ['path',        stepPath],
+  // 'path' is dispatched via applyPath (needs the registry for by(Traversal) projections)
   ['limit',       stepLimit],
   ['range',       stepRange],
   ['skip',        stepSkip],
diff --git a/gremlin-js/gremlin-javascript/lib/process/local/steps.ts b/gremlin-js/gremlin-javascript/lib/process/local/steps.ts
index 7ae0700683..ba2b04a5a0 100644
--- a/gremlin-js/gremlin-javascript/lib/process/local/steps.ts
+++ b/gremlin-js/gremlin-javascript/lib/process/local/steps.ts
@@ -19,7 +19,7 @@
 
 import { P, TextP, t, direction } from '../traversal.js';
 import { Graph, Vertex, Edge, VertexProperty, Property, Path } from '../../structure/graph.js';
-import type { Arg } from './types.js';
+import type { Arg, Pipeline } from './types.js';
 
 // ── Traverser helpers ─────────────────────────────────────────────────────────
 
@@ -487,13 +487,51 @@ export function* stepElementMap(
 
 // ── Path step ─────────────────────────────────────────────────────────────────
 
+/**
+ * Sentinel signalling a non-productive by() projection (e.g. by('age') on an element
+ * lacking the 'age' property). Without ProductiveByStrategy — which Tiny Gremlin does not
+ * support — a non-productive by() filters the whole path traverser.
+ */
+export const NON_PRODUCTIVE = Symbol('nonProductive');
+
+/** by(String)/by(T)/natural projection. Returns NON_PRODUCTIVE for an absent property. */
+function projectByKey(el: any, key: string | null | undefined): any {
+  if (key == null) return el;                              // natural / by()
+  if (key === 'T.id' || key === 'id') return el?.id;
+  if (key === 'T.label' || key === 'label') return el?.label;
+  if (el instanceof Vertex || el instanceof Edge) {
+    const vals = getValues(el, key);
+    return vals.length === 0 ? NON_PRODUCTIVE : vals[0];   // first value, multi-valued
+  }
+  return el;
+}
+
+// projections carry one entry per by() modulator, applied round-robin across path
+// elements. An entry is either a key (String/T/null for by(String)/by(T)/natural) or a
+// single-step sub-Pipeline (by(Traversal)), run via runTraversal. Empty when no by()
+// modulators are present, in which case the raw path objects are used.
 export function* stepPath(
-  source: Iterable<StreamItem>, args: Arg[], graph: Graph, trackPaths: boolean,
+  source: Iterable<StreamItem>,
+  projections: Arg[],
+  trackPaths: boolean,
+  runTraversal: (sub: Pipeline, object: any) => any,
 ): Generator<StreamItem> {
   for (const item of source) {
     const pathEntries: PathEntry[] = getPath(item, trackPaths);
     const labels: string[][] = pathEntries.map(e => e.labels);
-    const objects: any[] = pathEntries.map(e => e.object);
+    const objects: any[] = [];
+    let productive = true;
+    for (let idx = 0; idx < pathEntries.length; idx++) {
+      const object = pathEntries[idx].object;
+      if (projections.length === 0) { objects.push(object); continue; }
+      const proj = projections[idx % projections.length];
+      const value = Array.isArray(proj)
+        ? runTraversal(proj as unknown as Pipeline, object)
+        : projectByKey(object, proj as string | null | undefined);
+      if (value === NON_PRODUCTIVE) { productive = false; break; }
+      objects.push(value);
+    }
+    if (!productive) continue;   // default (non-ProductiveBy) semantics: filter the path
     const pathObj = new Path(labels, objects);
     // Yield as a traverser so toAsync's trackPaths unwrap produces the Path value correctly.
     yield trackPaths ? { value: pathObj, path: [] } : pathObj;
diff --git a/gremlin-js/gremlin-javascript/test/cucumber/gremlin.js b/gremlin-js/gremlin-javascript/test/cucumber/gremlin.js
index 73ef8f48f3..4b1e157bbb 100644
--- a/gremlin-js/gremlin-javascript/test/cucumber/gremlin.js
+++ b/gremlin-js/gremlin-javascript/test/cucumber/gremlin.js
@@ -1034,7 +1034,9 @@ const gremlins = {
     g_addV_propertyXset_emptyX: [function({g}) { return g.addV("foo").property(Cardinality.set, new Map([])) }, function({g}) { return g.V().hasLabel("person").values() }], 
     g_addVXpersonX_propertyXname_joshX_propertyXage_nullX: [function({g}) { return g.addV("person").property("name", "josh").property("age", null) }, function({g}) { return g.V().has("person", "age", null) }], 
     g_addVXpersonX_propertyXname_markoX_propertyXfriendWeight_null_acl_nullX: [function({g}) { return g.addV("person").property("name", "marko").property("friendWeight", null, "acl", null) }, function({g}) { return g.V().has("person", "name", "marko").has("friendWeight", null) }, function({g}) { return g.V().has("person", "name", "marko").properties("friendWeight").has("acl", null) }, function({g}) { return g.V().has("person", "name", "marko").properties("friendWeight").count() }], 
-    g_V_hasXperson_name_aliceX_propertyXsingle_age_unionXage_constantX1XX_sumX: [function({g}) { return g.addV("person").property("name", "alice").property("age", 50) }, function({g}) { return g.V().has("person", "name", "alice").property("age", 51) }, function({g}) { return g.V().has("person", "age", 50) }, function({g}) { return g.V().has("person", "age", 51) }],
+    g_V_hasXperson_name_aliceX_propertyXage_51X: [function({g}) { return g.addV("person").property("name", "alice").property("age", 50) }, function({g}) { return g.V().has("person", "name", "alice").property("age", 51) }, function({g}) { return g.V().has("person", "age", 50) }, function({g}) { return g.V().has("person", "age", 51) }], 
+    g_addVXpersonX_propertyXname_aliceX_propertyXage_30X_query: [function({g}) { return g.addV("person").property("name", "alice").property("age", 30) }, function({g}) { return g.V().has("person", "name", "alice") }, function({g}) { return g.V().has("person", "age", 30) }], 
+    g_V_hasXperson_name_aliceX_propertyXsingle_age_unionXage_constantX1XX_sumX: [function({g}) { return g.addV("person").property("name", "alice").property(Cardinality.single, "age", 50) }, function({g}) { return g.V().has("person", "name", "alice").property("age", __.union(__.values("age"), __.constant(1)).sum()) }, function({g}) { return g.V().has("person", "age", 50) }, function({g}) { return g.V().has("person", "age", 51) }], 
     g_V_limitX3X_addVXsoftwareX_aggregateXa1X_byXlabelX_aggregateXa2X_byXlabelX_capXa1_a2X_selectXa_bX_byXunfoldX_foldX: [function({g}) { return g.addV("person").property("name", "marko").property("age", 29).as("marko").addV("person").property("name", "vadas").property("age", 27).as("vadas").addV("software").property("name", "lop").property("lang", "java").as("lop").addV("person").property("name", "josh").property("age", 32).as("josh").addV("software").property("name", "ripple").property [...]
     g_addV_propertyXname_markoX_withXkey_valueX_valuesXname_keyX: [function({g}) { return g.addV().property("name", "marko").with_("key", "value").values("name", "key") }], 
     g_addV_propertyXname_marko_since_2010X_withXkey_valueX_propertiesXnameX_valuesXsince_keyX: [function({g}) { return g.addV().property("name", "marko", "since", 2010).with_("key", "value").properties("name").values("since", "key") }], 
@@ -1667,6 +1669,8 @@ const gremlins = {
     g_VX1X_outEXcreatedX_inV_inE_outV_path: [function({g, vid1}) { return g.V(vid1).outE("created").inV().inE().outV().path() }], 
     g_V_asXaX_out_asXbX_out_asXcX_path_fromXbX_toXcX_byXnameX: [function({g}) { return g.V().as("a").out().as("b").out().as("c").path().from_("b").to("c").by("name") }], 
     g_VX1X_out_path_byXageX: [function({g, vid1}) { return g.V(vid1).out().path().by("age") }], 
+    g_VX1X_out_path_byXlabelX: [function({g, vid1}) { return g.V(vid1).out().path().by(T.label) }], 
+    g_VX1X_out_path_byXvaluesXnameXX: [function({g, vid1}) { return g.V(vid1).out().path().by(__.values("name")) }], 
     g_withStrategiesXProductiveByStrategyX_VX1X_out_path_byXageX: [function({g, vid1}) { return g.withStrategies(new ProductiveByStrategy()).V(vid1).out().path().by("age") }], 
     g_injectX1_null_nullX_path: [function({g}) { return g.inject(1, null, null).path() }], 
     g_injectX1_null_nullX_path_dedup: [function({g}) { return g.inject(1, null, null).path().dedup() }], 
diff --git a/gremlin-js/gremlin-javascript/test/integration/tiny-gremlin-subgraph-tests.js b/gremlin-js/gremlin-javascript/test/integration/tiny-gremlin-subgraph-tests.js
new file mode 100644
index 0000000000..7ff34e194c
--- /dev/null
+++ b/gremlin-js/gremlin-javascript/test/integration/tiny-gremlin-subgraph-tests.js
@@ -0,0 +1,218 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+
+import assert from 'assert';
+import anon from '../../lib/process/anonymous-traversal.js';
+import { getConnection } from '../helper.js';
+import { Graph, Vertex, Edge } from '../../lib/structure/graph.js';
+import { t } from '../../lib/process/traversal.js';
+
+/**
+ * Exercises a core Tiny Gremlin use case end-to-end: pull a subgraph() from a remote
+ * graph, then run local in-process traversals over the detached Graph returned to the client.
+ *
+ * The "created" subgraph of the modern graph contains 5 vertices
+ * (marko, josh, peter — person; lop, ripple — software; vadas is excluded) and 4 created
+ * edges (marko->lop, josh->ripple, josh->lop, peter->lop).
+ */
+describe('Tiny Gremlin over subgraph()', function () {
+  let connection;
+  let sg;        // detached Graph returned by subgraph()
+  let lg;        // local Tiny Gremlin traversal source over sg
+  let markoId;   // marko's id from the remote graph, for id-preservation checks
+
+  const sorted = (arr) => [...arr].sort();
+  const pathObjects = (paths) => paths.map((p) => p.objects);
+
+  before(async function () {
+    connection = getConnection('gmodern');
+    await connection.open();
+    const g = anon.traversal().with_(connection);
+    sg = (await g.E().hasLabel('created').subgraph('sg').cap('sg').next()).value;
+    lg = anon.traversal().with_(sg);
+    markoId = (await g.V().has('name', 'marko').id().next()).value;
+  });
+
+  after(function () {
+    return connection.close();
+  });
+
+  it('returns a detached Graph from subgraph()', function () {
+    assert.ok(sg instanceof Graph);
+  });
+
+  it('captures the created-subgraph vertex set', async function () {
+    const vertices = await lg.V().toList();
+    assert.strictEqual(vertices.length, 5);
+    vertices.forEach((v) => assert.ok(v instanceof Vertex));
+    const names = sorted(await lg.V().values('name').toList());
+    assert.deepStrictEqual(names, ['josh', 'lop', 'marko', 'peter', 'ripple']);
+    assert.ok(!names.includes('vadas'), 'vadas has no created edges and must be excluded');
+  });
+
+  it('captures the created edges', async function () {
+    const edges = await lg.E().toList();
+    assert.strictEqual(edges.length, 4);
+    edges.forEach((e) => assert.ok(e instanceof Edge));
+    const labels = await lg.E().label().toList();
+    assert.deepStrictEqual(sorted(labels), ['created', 'created', 'created', 'created']);
+  });
+
+  it('filters by label locally', async function () {
+    const software = sorted(await lg.V().hasLabel('software').values('name').toList());
+    assert.deepStrictEqual(software, ['lop', 'ripple']);
+    const people = sorted(await lg.V().hasLabel('person').values('name').toList());
+    assert.deepStrictEqual(people, ['josh', 'marko', 'peter']);
+  });
+
+  it('preserves vertex properties through the round-trip', async function () {
+    assert.deepStrictEqual(await lg.V().has('name', 'marko').values('age').toList(), [29]);
+    assert.deepStrictEqual(await lg.V().has('name', 'josh').values('age').toList(), [32]);
+    const langs = await lg.V().hasLabel('software').values('lang').toList();
+    assert.deepStrictEqual(sorted(langs), ['java', 'java']);
+  });
+
+  it('navigates out-edges locally', async function () {
+    const joshOut = sorted(await lg.V().has('name', 'josh').out().values('name').toList());
+    assert.deepStrictEqual(joshOut, ['lop', 'ripple']);
+    const markoOut = await lg.V().has('name', 'marko').out().values('name').toList();
+    assert.deepStrictEqual(markoOut, ['lop']);
+  });
+
+  it('navigates in-edges locally', async function () {
+    const lopIn = sorted(await lg.V().has('name', 'lop').in_().values('name').toList());
+    assert.deepStrictEqual(lopIn, ['josh', 'marko', 'peter']);
+    const rippleIn = await lg.V().has('name', 'ripple').in_().values('name').toList();
+    assert.deepStrictEqual(rippleIn, ['josh']);
+  });
+
+  it('preserves edge properties', async function () {
+    // weight may serialize as float; compare within tolerance rather than exact equality
+    const near = (actual, expected) => assert.ok(Math.abs(actual - expected) < 1e-6, `${actual} ~ ${expected}`);
+    const markoWeight = await lg.V().has('name', 'marko').outE().values('weight').toList();
+    assert.strictEqual(markoWeight.length, 1);
+    near(markoWeight[0], 0.4);
+    const weights = (await lg.E().values('weight').toList()).map(Number).sort((a, b) => a - b);
+    assert.strictEqual(weights.length, 4);
+    [0.2, 0.4, 0.4, 1.0].forEach((expected, i) => near(weights[i], expected));
+  });
+
+  it('orders results locally', async function () {
+    const ordered = await lg.V().hasLabel('person').values('name').order().toList();
+    assert.deepStrictEqual(ordered, ['josh', 'marko', 'peter']);
+  });
+
+  it('projects paths with by() over the subgraph', async function () {
+    const byName = pathObjects(await lg.V().has('name', 'josh').out().path().by('name').toList());
+    assert.strictEqual(byName.length, 2);
+    byName.forEach((p) => assert.strictEqual(p[0], 'josh'));
+    assert.deepStrictEqual(sorted(byName.map((p) => p[1])), ['lop', 'ripple']);
+    const byLabel = pathObjects(await lg.V().has('name', 'marko').out().path().by(t.label).toList());
+    assert.deepStrictEqual(byLabel, [['person', 'software']]);
+  });
+
+  it('preserves element ids for direct lookup', async function () {
+    const localMarkoId = (await lg.V().has('name', 'marko').id().next()).value;
+    assert.deepStrictEqual(localMarkoId, markoId);
+    const byId = await lg.V(markoId).values('name').toList();
+    assert.deepStrictEqual(byId, ['marko']);
+  });
+
+  it('returns elementMap with tokens and properties', async function () {
+    const m = (await lg.V().has('name', 'marko').elementMap().next()).value;
+    assert.strictEqual(m.get('name'), 'marko');
+    assert.strictEqual(m.get('age'), 29);
+    assert.strictEqual(m.get(t.label), 'person');
+    assert.deepStrictEqual(m.get(t.id), markoId);
+  });
+
+  // Tiny Gremlin is a subset — steps like project(), group(), groupCount(), sum() and fold()
+  // are unavailable. The pattern is to do everything the subset DOES support inside the
+  // traversal (here, sort with order().by(...)), terminate with toList(), then finish in
+  // ordinary JavaScript with the array methods the subset lacks. forEach()/reduce() are
+  // native Tiny Gremlin terminals; map()/filter()/flat() run on the materialized array.
+  describe('post-processing with JavaScript closures', function () {
+    it('projects with Array.map() in place of project()', async function () {
+      // order() does the sorting; only the projection has no Tiny Gremlin equivalent
+      const projected = (await lg.V().hasLabel('person').order().by('name').elementMap().toList())
+        .map((m) => ({ name: m.get('name'), age: m.get('age') }));
+      assert.deepStrictEqual(projected, [
+        { name: 'josh', age: 32 },
+        { name: 'marko', age: 29 },
+        { name: 'peter', age: 35 },
+      ]);
+    });
+
+    it('filters with Array.filter() on a JS-only predicate', async function () {
+      // name length has no Tiny Gremlin predicate, so the filter must live in JS
+      const longNames = (await lg.V().hasLabel('person').order().by('name').elementMap().toList())
+        .filter((m) => m.get('name').length > 4)
+        .map((m) => m.get('name'));
+      assert.deepStrictEqual(longNames, ['marko', 'peter']);
+    });
+
+    it('fans out and flattens with Array.flat() in place of flatMap()', async function () {
+      const people = await lg.V().hasLabel('person').order().by('name').values('name').toList();
+      const created = (
+        await Promise.all(
+          people.map(async (name) => {
+            const items = await lg.V().has('name', name).out().order().by('name').values('name').toList();
+            return items.map((item) => `${name}->${item}`);
+          }),
+        )
+      ).flat();
+      assert.deepStrictEqual(created, ['josh->lop', 'josh->ripple', 'marko->lop', 'peter->lop']);
+    });
+
+    it('aggregates with the reduce() terminal in place of sum()', async function () {
+      const totalAge = await lg.V().hasLabel('person').values('age').reduce((acc, age) => acc + age, 0);
+      assert.strictEqual(totalAge, 29 + 32 + 35);
+    });
+
+    it('group-counts with Array.reduce() in place of groupCount()', async function () {
+      const labels = await lg.V().label().toList();
+      const counts = labels.reduce((acc, label) => {
+        acc[label] = (acc[label] || 0) + 1;
+        return acc;
+      }, {});
+      assert.deepStrictEqual(counts, { person: 3, software: 2 });
+    });
+
+    it('groups into buckets with Array.reduce() in place of group()', async function () {
+      // order().by('name') means each bucket comes out name-sorted with no JS sort
+      const maps = await lg.V().order().by('name').elementMap().toList();
+      const byLabel = maps.reduce((acc, m) => {
+        const label = m.get(t.label);
+        (acc[label] = acc[label] || []).push(m.get('name'));
+        return acc;
+      }, {});
+      assert.deepStrictEqual(byLabel, {
+        person: ['josh', 'marko', 'peter'],
+        software: ['lop', 'ripple'],
+      });
+    });
+
+    it('collects via the forEach() terminal for side-effecting consumption', async function () {
+      const collected = [];
+      await lg.V().hasLabel('software').order().by('name').values('name')
+        .forEach((name) => collected.push(name.toUpperCase()));
+      assert.deepStrictEqual(collected, ['LOP', 'RIPPLE']);
+    });
+  });
+});
diff --git a/gremlin-js/gremlin-javascript/test/unit/tiny-gremlin-path-by-test.js b/gremlin-js/gremlin-javascript/test/unit/tiny-gremlin-path-by-test.js
new file mode 100644
index 0000000000..b8e9b750ed
--- /dev/null
+++ b/gremlin-js/gremlin-javascript/test/unit/tiny-gremlin-path-by-test.js
@@ -0,0 +1,75 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+
+import { assert } from 'chai';
+import anon from '../../lib/process/anonymous-traversal.js';
+import { statics as __ } from '../../lib/process/graph-traversal.js';
+import { t } from '../../lib/process/traversal.js';
+import { buildModernGraph } from '../cucumber/local-fixtures.js';
+
+/**
+ * Unit coverage for path().by() projection forms that the @TinyGremlin Gherkin
+ * scenarios do not exercise: by(T), the limited by(Traversal), and the rejection
+ * of unsupported by(Traversal) shapes. by(String) round-robin and non-productive
+ * filtering are covered by Path.feature.
+ */
+describe('Tiny Gremlin path().by()', () => {
+  let g;
+  const markoId = 1; // modern graph: makeVertex(1, 'person', ...)
+
+  beforeEach(() => {
+    g = anon.traversal().with_(buildModernGraph().graph);
+  });
+
+  const objectsOf = (paths) => paths.map((p) => p.objects);
+
+  it('by(String) filters non-productive elements (lop has no age)', async () => {
+    const result = await g.V(markoId).out().path().by('age').toList();
+    // marko(29) -> {vadas(27), josh(32), lop(no age -> path filtered)}
+    assert.sameDeepMembers(objectsOf(result), [[29, 27], [29, 32]]);
+  });
+
+  it('by(T) projects element id/label', async () => {
+    const result = await g.V(markoId).out().path().by(t.label).toList();
+    // marko=person, out-neighbours: vadas/josh=person, lop=software
+    assert.sameDeepMembers(objectsOf(result), [
+      ['person', 'person'],
+      ['person', 'person'],
+      ['person', 'software'],
+    ]);
+  });
+
+  it('by(Traversal) with a single value-extraction step (values) takes the first value', async () => {
+    const result = await g.V(markoId).out().path().by(__.values('name')).toList();
+    assert.sameDeepMembers(objectsOf(result), [
+      ['marko', 'vadas'],
+      ['marko', 'josh'],
+      ['marko', 'lop'],
+    ]);
+  });
+
+  it('rejects by(Traversal) that is not a single value-extraction step', async () => {
+    try {
+      await g.V(markoId).out().path().by(__.out().values('name')).toList();
+      assert.fail('expected a LocalExecutionError for a multi-step by(Traversal)');
+    } catch (err) {
+      assert.match(err.message, /single value-extraction step/);
+    }
+  });
+});
diff --git a/gremlin-python/src/main/python/tests/feature/gremlin.py b/gremlin-python/src/main/python/tests/feature/gremlin.py
index 6ce019b0c0..52c2651eda 100644
--- a/gremlin-python/src/main/python/tests/feature/gremlin.py
+++ b/gremlin-python/src/main/python/tests/feature/gremlin.py
@@ -1008,6 +1008,8 @@ world.gremlins = {
     'g_addV_propertyXset_emptyX': [(lambda g:g.add_v('foo').property(Cardinality.set_, {  })), (lambda g:g.V().has_label('person').values())], 
     'g_addVXpersonX_propertyXname_joshX_propertyXage_nullX': [(lambda g:g.add_v('person').property('name', 'josh').property('age', None)), (lambda g:g.V().has('person', 'age', None))], 
     'g_addVXpersonX_propertyXname_markoX_propertyXfriendWeight_null_acl_nullX': [(lambda g:g.add_v('person').property('name', 'marko').property('friendWeight', None, 'acl', None)), (lambda g:g.V().has('person', 'name', 'marko').has('friendWeight', None)), (lambda g:g.V().has('person', 'name', 'marko').properties('friendWeight').has('acl', None)), (lambda g:g.V().has('person', 'name', 'marko').properties('friendWeight').count())], 
+    'g_V_hasXperson_name_aliceX_propertyXage_51X': [(lambda g:g.add_v('person').property('name', 'alice').property('age', 50)), (lambda g:g.V().has('person', 'name', 'alice').property('age', 51)), (lambda g:g.V().has('person', 'age', 50)), (lambda g:g.V().has('person', 'age', 51))], 
+    'g_addVXpersonX_propertyXname_aliceX_propertyXage_30X_query': [(lambda g:g.add_v('person').property('name', 'alice').property('age', 30)), (lambda g:g.V().has('person', 'name', 'alice')), (lambda g:g.V().has('person', 'age', 30))], 
     'g_V_hasXperson_name_aliceX_propertyXsingle_age_unionXage_constantX1XX_sumX': [(lambda g:g.add_v('person').property('name', 'alice').property(Cardinality.single, 'age', 50)), (lambda g:g.V().has('person', 'name', 'alice').property('age', __.union(__.values('age'), __.constant(1)).sum_())), (lambda g:g.V().has('person', 'age', 50)), (lambda g:g.V().has('person', 'age', 51))], 
     'g_V_limitX3X_addVXsoftwareX_aggregateXa1X_byXlabelX_aggregateXa2X_byXlabelX_capXa1_a2X_selectXa_bX_byXunfoldX_foldX': [(lambda g:g.add_v('person').property('name', 'marko').property('age', 29).as_('marko').add_v('person').property('name', 'vadas').property('age', 27).as_('vadas').add_v('software').property('name', 'lop').property('lang', 'java').as_('lop').add_v('person').property('name', 'josh').property('age', 32).as_('josh').add_v('software').property('name', 'ripple').property(' [...]
     'g_addV_propertyXname_markoX_withXkey_valueX_valuesXname_keyX': [(lambda g:g.add_v().property('name', 'marko').with_('key', 'value').values('name', 'key'))], 
@@ -1641,6 +1643,8 @@ world.gremlins = {
     'g_VX1X_outEXcreatedX_inV_inE_outV_path': [(lambda g, vid1=None:g.V(vid1).out_e('created').in_v().in_e().out_v().path())], 
     'g_V_asXaX_out_asXbX_out_asXcX_path_fromXbX_toXcX_byXnameX': [(lambda g:g.V().as_('a').out().as_('b').out().as_('c').path().from_('b').to('c').by('name'))], 
     'g_VX1X_out_path_byXageX': [(lambda g, vid1=None:g.V(vid1).out().path().by('age'))], 
+    'g_VX1X_out_path_byXlabelX': [(lambda g, vid1=None:g.V(vid1).out().path().by(T.label))], 
+    'g_VX1X_out_path_byXvaluesXnameXX': [(lambda g, vid1=None:g.V(vid1).out().path().by(__.values('name')))], 
     'g_withStrategiesXProductiveByStrategyX_VX1X_out_path_byXageX': [(lambda g, vid1=None:g.with_strategies(ProductiveByStrategy()).V(vid1).out().path().by('age'))], 
     'g_injectX1_null_nullX_path': [(lambda g:g.inject(1, None, None).path())], 
     'g_injectX1_null_nullX_path_dedup': [(lambda g:g.inject(1, None, None).path().dedup())], 
diff --git a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/language/translator/translations.json b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/language/translator/translations.json
index d7e90db6b3..e701f0f03d 100644
--- a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/language/translator/translations.json
+++ b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/language/translator/translations.json
@@ -20551,6 +20551,100 @@
             }
         ]
     },
+    {
+        "scenario": "g_V_hasXperson_name_aliceX_propertyXage_51X",
+        "traversals": [
+            {
+                "original": "g.addV(\"person\").property(\"name\", \"alice\").property(\"age\", 50)",
+                "language": "g.addV(\"person\").property(\"name\", \"alice\").property(\"age\", 50)",
+                "canonical": "g.addV(\"person\").property(\"name\", \"alice\").property(\"age\", 50)",
+                "anonymized": "g.addV(string0).property(string1, string2).property(string3, number0)",
+                "dotnet": "g.AddV((string) \"person\").Property(\"name\", \"alice\").Property(\"age\", 50)",
+                "go": "g.AddV(\"person\").Property(\"name\", \"alice\").Property(\"age\", 50)",
+                "groovy": "g.addV(\"person\").property(\"name\", \"alice\").property(\"age\", 50)",
+                "java": "g.addV(\"person\").property(\"name\", \"alice\").property(\"age\", 50)",
+                "javascript": "g.addV(\"person\").property(\"name\", \"alice\").property(\"age\", 50)",
+                "python": "g.add_v('person').property('name', 'alice').property('age', 50)"
+            },
+            {
+                "original": "g.V().has(\"person\",\"name\",\"alice\").property(\"age\", 51)",
+                "language": "g.V().has(\"person\", \"name\", \"alice\").property(\"age\", 51)",
+                "canonical": "g.V().has(\"person\", \"name\", \"alice\").property(\"age\", 51)",
+                "anonymized": "g.V().has(string0, string1, string2).property(string3, number0)",
+                "dotnet": "g.V().Has(\"person\", \"name\", \"alice\").Property(\"age\", 51)",
+                "go": "g.V().Has(\"person\", \"name\", \"alice\").Property(\"age\", 51)",
+                "groovy": "g.V().has(\"person\", \"name\", \"alice\").property(\"age\", 51)",
+                "java": "g.V().has(\"person\", \"name\", \"alice\").property(\"age\", 51)",
+                "javascript": "g.V().has(\"person\", \"name\", \"alice\").property(\"age\", 51)",
+                "python": "g.V().has('person', 'name', 'alice').property('age', 51)"
+            },
+            {
+                "original": "g.V().has(\"person\",\"age\",50)",
+                "language": "g.V().has(\"person\", \"age\", 50)",
+                "canonical": "g.V().has(\"person\", \"age\", 50)",
+                "anonymized": "g.V().has(string0, string1, number0)",
+                "dotnet": "g.V().Has(\"person\", \"age\", 50)",
+                "go": "g.V().Has(\"person\", \"age\", 50)",
+                "groovy": "g.V().has(\"person\", \"age\", 50)",
+                "java": "g.V().has(\"person\", \"age\", 50)",
+                "javascript": "g.V().has(\"person\", \"age\", 50)",
+                "python": "g.V().has('person', 'age', 50)"
+            },
+            {
+                "original": "g.V().has(\"person\",\"age\",51)",
+                "language": "g.V().has(\"person\", \"age\", 51)",
+                "canonical": "g.V().has(\"person\", \"age\", 51)",
+                "anonymized": "g.V().has(string0, string1, number0)",
+                "dotnet": "g.V().Has(\"person\", \"age\", 51)",
+                "go": "g.V().Has(\"person\", \"age\", 51)",
+                "groovy": "g.V().has(\"person\", \"age\", 51)",
+                "java": "g.V().has(\"person\", \"age\", 51)",
+                "javascript": "g.V().has(\"person\", \"age\", 51)",
+                "python": "g.V().has('person', 'age', 51)"
+            }
+        ]
+    },
+    {
+        "scenario": "g_addVXpersonX_propertyXname_aliceX_propertyXage_30X_query",
+        "traversals": [
+            {
+                "original": "g.addV(\"person\").property(\"name\", \"alice\").property(\"age\", 30)",
+                "language": "g.addV(\"person\").property(\"name\", \"alice\").property(\"age\", 30)",
+                "canonical": "g.addV(\"person\").property(\"name\", \"alice\").property(\"age\", 30)",
+                "anonymized": "g.addV(string0).property(string1, string2).property(string3, number0)",
+                "dotnet": "g.AddV((string) \"person\").Property(\"name\", \"alice\").Property(\"age\", 30)",
+                "go": "g.AddV(\"person\").Property(\"name\", \"alice\").Property(\"age\", 30)",
+                "groovy": "g.addV(\"person\").property(\"name\", \"alice\").property(\"age\", 30)",
+                "java": "g.addV(\"person\").property(\"name\", \"alice\").property(\"age\", 30)",
+                "javascript": "g.addV(\"person\").property(\"name\", \"alice\").property(\"age\", 30)",
+                "python": "g.add_v('person').property('name', 'alice').property('age', 30)"
+            },
+            {
+                "original": "g.V().has(\"person\",\"name\",\"alice\")",
+                "language": "g.V().has(\"person\", \"name\", \"alice\")",
+                "canonical": "g.V().has(\"person\", \"name\", \"alice\")",
+                "anonymized": "g.V().has(string0, string1, string2)",
+                "dotnet": "g.V().Has(\"person\", \"name\", \"alice\")",
+                "go": "g.V().Has(\"person\", \"name\", \"alice\")",
+                "groovy": "g.V().has(\"person\", \"name\", \"alice\")",
+                "java": "g.V().has(\"person\", \"name\", \"alice\")",
+                "javascript": "g.V().has(\"person\", \"name\", \"alice\")",
+                "python": "g.V().has('person', 'name', 'alice')"
+            },
+            {
+                "original": "g.V().has(\"person\",\"age\",30)",
+                "language": "g.V().has(\"person\", \"age\", 30)",
+                "canonical": "g.V().has(\"person\", \"age\", 30)",
+                "anonymized": "g.V().has(string0, string1, number0)",
+                "dotnet": "g.V().Has(\"person\", \"age\", 30)",
+                "go": "g.V().Has(\"person\", \"age\", 30)",
+                "groovy": "g.V().has(\"person\", \"age\", 30)",
+                "java": "g.V().has(\"person\", \"age\", 30)",
+                "javascript": "g.V().has(\"person\", \"age\", 30)",
+                "python": "g.V().has('person', 'age', 30)"
+            }
+        ]
+    },
     {
         "scenario": "g_V_hasXperson_name_aliceX_propertyXsingle_age_unionXage_constantX1XX_sumX",
         "traversals": [
@@ -35224,6 +35318,40 @@
             }
         ]
     },
+    {
+        "scenario": "g_VX1X_out_path_byXlabelX",
+        "traversals": [
+            {
+                "original": "g.V(vid1).out().path().by(T.label)",
+                "language": "g.V(vid1).out().path().by(T.label)",
+                "canonical": "g.V(vid1).out().path().by(T.label)",
+                "anonymized": "g.V(vid1).out().path().by(T.label)",
+                "dotnet": "g.V(vid1).Out().Path().By(T.Label)",
+                "go": "g.V(vid1).Out().Path().By(gremlingo.T.Label)",
+                "groovy": "g.V(vid1).out().path().by(T.label)",
+                "java": "g.V(vid1).out().path().by(T.label)",
+                "javascript": "g.V(vid1).out().path().by(T.label)",
+                "python": "g.V(vid1).out().path().by(T.label)"
+            }
+        ]
+    },
+    {
+        "scenario": "g_VX1X_out_path_byXvaluesXnameXX",
+        "traversals": [
+            {
+                "original": "g.V(vid1).out().path().by(__.values(\"name\"))",
+                "language": "g.V(vid1).out().path().by(__.values(\"name\"))",
+                "canonical": "g.V(vid1).out().path().by(__.values(\"name\"))",
+                "anonymized": "g.V(vid1).out().path().by(__.values(string0))",
+                "dotnet": "g.V(vid1).Out().Path().By(__.Values<object>(\"name\"))",
+                "go": "g.V(vid1).Out().Path().By(gremlingo.T__.Values(\"name\"))",
+                "groovy": "g.V(vid1).out().path().by(__.values(\"name\"))",
+                "java": "g.V(vid1).out().path().by(__.values(\"name\"))",
+                "javascript": "g.V(vid1).out().path().by(__.values(\"name\"))",
+                "python": "g.V(vid1).out().path().by(__.values('name'))"
+            }
+        ]
+    },
     {
         "scenario": "g_withStrategiesXProductiveByStrategyX_VX1X_out_path_byXageX",
         "traversals": [
diff --git a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/Path.feature b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/Path.feature
index adfdd7ccb1..d7a62ad900 100644
--- a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/Path.feature
+++ b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/Path.feature
@@ -31,7 +31,7 @@ Feature: Step - path()
       | result |
       | p[v[marko],marko] |
 
-  @GraphComputerVerificationReferenceOnly
+  @GraphComputerVerificationReferenceOnly @TinyGremlin
   Scenario: g_VX1X_out_path_byXageX_byXnameX
     Given the modern graph
     And using the parameter vid1 defined as "v[marko].id"
@@ -59,7 +59,7 @@ Feature: Step - path()
       | p[v[marko],josh,java] |
       | p[v[marko],josh,java] |
 
-  @GraphComputerVerificationReferenceOnly
+  @GraphComputerVerificationReferenceOnly @TinyGremlin
   Scenario: g_V_out_out_path_byXnameX_byXageX
     Given the modern graph
     And the traversal of
@@ -111,7 +111,7 @@ Feature: Step - path()
       | p[josh,ripple] |
       | p[josh,lop] |
 
-  @GraphComputerVerificationReferenceOnly
+  @GraphComputerVerificationReferenceOnly @TinyGremlin
   Scenario: g_VX1X_out_path_byXageX
     Given the modern graph
     And using the parameter vid1 defined as "v[marko].id"
@@ -125,6 +125,36 @@ Feature: Step - path()
       | p[d[29].i,d[27].i] |
       | p[d[29].i,d[32].i] |
 
+  @GraphComputerVerificationReferenceOnly @TinyGremlin
+  Scenario: g_VX1X_out_path_byXlabelX
+    Given the modern graph
+    And using the parameter vid1 defined as "v[marko].id"
+    And the traversal of
+      """
+      g.V(vid1).out().path().by(T.label)
+      """
+    When iterated to list
+    Then the result should be unordered
+      | result |
+      | p[person,person] |
+      | p[person,person] |
+      | p[person,software] |
+
+  @GraphComputerVerificationReferenceOnly @TinyGremlin
+  Scenario: g_VX1X_out_path_byXvaluesXnameXX
+    Given the modern graph
+    And using the parameter vid1 defined as "v[marko].id"
+    And the traversal of
+      """
+      g.V(vid1).out().path().by(__.values("name"))
+      """
+    When iterated to list
+    Then the result should be unordered
+      | result |
+      | p[marko,vadas] |
+      | p[marko,josh] |
+      | p[marko,lop] |
+
   @GraphComputerVerificationReferenceOnly @WithProductiveByStrategy
   Scenario: g_withStrategiesXProductiveByStrategyX_VX1X_out_path_byXageX
     Given the modern graph


(tinkerpop) 05/06: Add repeat() to gremlin-javascript Tiny Gremlin

Posted by sp...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

spmallette pushed a commit to branch jsprocess
in repository https://gitbox.apache.org/repos/asf/tinkerpop.git

commit c35e817ad63ccaf72f61784fabdd3ea2e6a0bf52
Author: Stephen Mallette <st...@amazon.com>
AuthorDate: Tue Jun 30 10:56:34 2026 -0400

    Add repeat() to gremlin-javascript Tiny Gremlin
    
    Implements repeat() with times()/until()/emit() modulators in the local
    in-process executor, including position-sensitive while vs. do-while
    semantics, emit side-output, and nested repeat() to any depth.
    
    Refactors the executor around an ExecutionContext (a lightweight analog of
    Java's TraversalParent) so branching steps run their child pipelines through
    runBranch/runProject/runRooted rather than bespoke escapes; path, addE, and
    repeat become parent-step registry entries.
    
    The loop is driven breadth-first over the whole frontier, so barrier steps
    such as order().by() inside the body see every traverser at that loop
    position and order globally, matching full Gremlin at any nesting depth.
    
    The loop-label form, the predicate form of until()/emit(), and mutation
    steps within the loop are rejected with clear messages.
    
    Assisted-by: Claude Code:claude-opus-4-8
---
 CHANGELOG.asciidoc                                 |   1 -
 docs/src/dev/provider/tiny-gremlin.asciidoc        |  53 +++-
 docs/src/reference/gremlin-variants.asciidoc       |   3 +
 docs/src/reference/the-traversal.asciidoc          |   1 +
 .../lib/language/executor/ExecuteVisitor.ts        |  36 +++
 .../lib/process/local/LocalExecutor.ts             | 278 ++++++++++++++++-----
 .../gremlin-javascript/lib/process/local/passes.ts |   1 +
 .../gremlin-javascript/lib/process/local/steps.ts  |  78 +++++-
 .../gremlin-javascript/lib/process/local/types.ts  |  41 +++
 .../test/unit/tiny-gremlin-repeat-test.js          | 170 +++++++++++++
 .../gremlin/test/features/branch/Repeat.feature    |  22 +-
 11 files changed, 606 insertions(+), 78 deletions(-)

diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc
index b5c70c2d21..963ad54f5c 100644
--- a/CHANGELOG.asciidoc
+++ b/CHANGELOG.asciidoc
@@ -130,7 +130,6 @@ image::https://raw.githubusercontent.com/apache/tinkerpop/master/docs/static/ima
 * Added the `gql-gremlin` module containing the TinkerGQL engine, a `MATCH`-pattern executor implementing a deliberate minimal subset of ISO GQL, usable by any graph provider.
 * Added `Graph.countVerticesByLabel(String)`, `Graph.countEdgesByLabel(String)`, and the `Graph.Index` nested interface as performance-hint extension points for the TinkerGQL engine.
 * Added GQL support to TinkerGraph with `gql-gremlin`,
-* Added Tiny Gremlin local execution to `gremlin-javascript`: `traversal().with_(graph)` now executes a defined subset of Gremlin steps (navigation, filtering, value extraction, path tracking, range, ordering, and local mutation) against an in-memory `Graph` without a remote server. The `Graph` adjacency index (`outEdges`, `inEdges`) enables O(1) navigation. New `forEach(fn)` and `reduce(fn, seed)` terminal methods on `Traversal` support result processing with native JavaScript functions [...]
 
 [[release-4-0-0-beta-2]]
 === TinkerPop 4.0.0-beta.2 (April 1, 2026)
diff --git a/docs/src/dev/provider/tiny-gremlin.asciidoc b/docs/src/dev/provider/tiny-gremlin.asciidoc
index bffd26056e..1bbb310827 100644
--- a/docs/src/dev/provider/tiny-gremlin.asciidoc
+++ b/docs/src/dev/provider/tiny-gremlin.asciidoc
@@ -37,12 +37,12 @@ then navigates and filters it locally using familiar Gremlin syntax. A secondary
 entirely in process for lightweight analysis or testing, using `addV()`, `addE()`, and `property()`.
 
 Because the graph is small and entirely in memory, features that are expensive or architecture-dependent in a full
-implementation, such as `repeat()`, distributed OLAP steps, aggregation barriers, and strategy-based optimizations,
-are deliberately excluded. The result is a small, self-contained, predictable execution model.
+implementation, such as distributed OLAP steps, aggregation barriers, and strategy-based optimizations, are
+deliberately excluded. The result is a small, self-contained, predictable execution model.
 
 === Supported Steps
 
-Tiny Gremlin supports exactly the 33 steps listed below, grouped by category. No other step may be used in a Tiny
+Tiny Gremlin supports exactly the 37 steps listed below, grouped by category. No other step may be used in a Tiny
 Gremlin traversal; doing so results in an error before any graph data is accessed.
 
 [width="100%",options="header"]
@@ -55,8 +55,9 @@ Gremlin traversal; doing so results in an error before any graph data is accesse
 |Path |`path()`
 |Range |`limit()`, `range()`, `skip()`, `tail()`
 |Ordering |`order()`
+|Branch |`repeat()`
 |Mutation |`addV()`, `addE()`, `property()`
-|Modulators |`from()`, `to()`
+|Modulators |`from()`, `to()`, `times()`, `until()`, `emit()`
 |===
 
 === Terminal Methods
@@ -347,6 +348,50 @@ Emits elements at positions `[low, high)` in the stream, zero-indexed.
 
 * The `Scope` form is not supported and will raise an error.
 
+=== repeat()
+
+*Gremlin Semantics reference:* <<repeat-step,repeat()>>
+
+Repeatedly applies a child traversal to each traverser, forming a loop. The loop is bounded by a `times()` or
+`until()` modulator, and `emit()` may side-output traversers as they pass through the loop. The position of a
+modulator relative to `repeat()` determines its semantics, matching full Gremlin:
+
+* `repeat(traversal).times(n)` runs the loop body `n` times. Declared after `repeat()`, the body always executes at
+  least once, so `times(0)` still runs the body once. Declared before, as in `times(n).repeat(traversal)`, the count
+  is checked first, so `times(0)` emits the input unchanged without running the body.
+* `repeat(traversal).until(condition)` is do-while: the body runs and then the condition is tested. Reversing the
+  order to `until(condition).repeat(traversal)` is while: the condition is tested before the body.
+* `emit()` side-outputs every traverser, and `emit(condition)` side-outputs only those satisfying the condition.
+  Declared after `repeat()` it emits traversers leaving the body, and declared before it emits them entering the body.
+
+A `condition` for `until()` or `emit()` is an anonymous filter traversal, satisfied when it produces at least one
+result. The loop body and these conditions may themselves contain any supported step, including `order().by()` or
+`path()`.
+
+The loop is driven breadth-first over the whole frontier: at each loop iteration every surviving traverser advances
+in lockstep through one shared evaluation of the body. A barrier step such as `order().by()` inside the body
+therefore sees all traversers at that loop position and orders them globally, matching full Gremlin rather than
+ordering within the descendants of a single input.
+
+Nested `repeat()` is supported. A `repeat()` may appear inside another `repeat()`, whether in the loop body or in
+an `until()` or `emit()` condition, to any depth. For example,
+`repeat(__.out().repeat(__.in()).times(2)).until(__.hasId(10))` is valid, and the global barrier behaviour above is
+preserved through the nesting, so `order().by()` within an inner `repeat()` still orders the whole frontier.
+
+*Tiny Gremlin limitations:*
+
+* The loop-label form (`repeat("label", traversal)`) is not supported because `as()`-style step labels and the
+  `loops("label")` reference are excluded from Tiny Gremlin.
+* The predicate form (`until(P)` and `emit(P)`) is not supported. It corresponds to Java's `Predicate<Traverser>`
+  lambda overload rather than a value predicate, so the anonymous-traversal form (for example
+  `until(__.has("name", "ripple"))`) must be used instead.
+* Mutation steps (`addV()`, `addE()`, `property()`) are not permitted within a `repeat()` body or its `until()` and
+  `emit()` conditions, since mutating the graph while looping over it has no well-defined semantics. This is enforced
+  at any nesting depth, including inside a nested `repeat()`.
+* A `repeat()` with no `times()` or `until()` bound terminates only when its body stops producing traversers, as on
+  an acyclic graph. As in full Gremlin, an unbounded loop over cyclic data does not terminate, so a bound is the
+  caller's responsibility.
+
 === skip()
 
 *Gremlin Semantics reference:* <<skip-step,skip()>>
diff --git a/docs/src/reference/gremlin-variants.asciidoc b/docs/src/reference/gremlin-variants.asciidoc
index 4256fe0a32..6e96938a45 100644
--- a/docs/src/reference/gremlin-variants.asciidoc
+++ b/docs/src/reference/gremlin-variants.asciidoc
@@ -2243,6 +2243,9 @@ const totalAge = await g.V().hasLabel('person').values('age').reduce((acc, age)
 
 // path() projects each element through by()
 const paths = await g.V().has('name', 'marko').out().path().by('name').toList();
+
+// repeat() loops a child traversal, here two hops out from marko
+const reached = await g.V().has('name', 'marko').repeat(__.out()).times(2).values('name').toList();
 ----
 
 ==== Working within the subset
diff --git a/docs/src/reference/the-traversal.asciidoc b/docs/src/reference/the-traversal.asciidoc
index 844bdfa8bd..c5804d2c65 100644
--- a/docs/src/reference/the-traversal.asciidoc
+++ b/docs/src/reference/the-traversal.asciidoc
@@ -6889,6 +6889,7 @@ graph analysis tasks.
 |Value Extraction |<<id-step,`id()`>>, <<label-step,`label()`>>, <<values-step,`values()`>>, <<valuemap-step,`valueMap()`>>, <<elementmap-step,`elementMap()`>>, <<value-step,`value()`>>, <<key-step,`key()`>>
 |Path |<<path-step,`path()`>>
 |Range |<<limit-step,`limit()`>>, <<range-step,`range()`>>, <<skip-step,`skip()`>>, <<tail-step,`tail()`>>
+|Branching | <<repeat-step,`repeat()`>>, <<emit-step, `emit()`>>, <<times-step, `times()`>>, <<until-step, `until()`>>
 |Ordering |<<order-step,`order()`>>
 |Mutation |<<addvertex-step,`addV()`>>, <<addedge-step,`addE()`>>, <<property-step,`property()`>>
 |Modulators |<<from-step,`from()`>>, <<to-step,`to()`>> - in conjunction with `addE()`, not `path()`.
diff --git a/gremlin-js/gremlin-javascript/lib/language/executor/ExecuteVisitor.ts b/gremlin-js/gremlin-javascript/lib/language/executor/ExecuteVisitor.ts
index 37c5808196..84b77dc3e4 100644
--- a/gremlin-js/gremlin-javascript/lib/language/executor/ExecuteVisitor.ts
+++ b/gremlin-js/gremlin-javascript/lib/language/executor/ExecuteVisitor.ts
@@ -480,6 +480,42 @@ export class ExecuteVisitor {
     }
   }
 
+  // ── Repeat / loop steps ───────────────────────────────────────────────────
+  // repeat() and its times()/until()/emit() modulators are pushed as separate
+  // descriptors here; the local executor folds them into a single repeat spec,
+  // tracking whether each modulator was declared before or after the repeat().
+
+  visitTraversalMethod_repeat_Traversal(ctx: any): void {
+    this.push('repeat', [this.visitNestedTraversalAsSubPipeline(ctx.nestedTraversal())]);
+  }
+
+  // repeat(String, Traversal) — the loop-label form. Tiny Gremlin has no step labels,
+  // so this is left without a body so the executor can reject it with a clear message.
+
+  visitTraversalMethod_times(ctx: any): void {
+    this.push('times', [this.recoverIntegerLiteral(ctx.integerLiteral())]);
+  }
+
+  visitTraversalMethod_until_Traversal(ctx: any): void {
+    this.push('until', [this.visitNestedTraversalAsSubPipeline(ctx.nestedTraversal())]);
+  }
+
+  visitTraversalMethod_until_Predicate(ctx: any): void {
+    this.push('until', [this.recoverPredicate(ctx.traversalPredicate())]);
+  }
+
+  visitTraversalMethod_emit_Empty(_ctx: any): void {
+    this.push('emit', []);
+  }
+
+  visitTraversalMethod_emit_Traversal(ctx: any): void {
+    this.push('emit', [this.visitNestedTraversalAsSubPipeline(ctx.nestedTraversal())]);
+  }
+
+  visitTraversalMethod_emit_Predicate(ctx: any): void {
+    this.push('emit', [this.recoverPredicate(ctx.traversalPredicate())]);
+  }
+
   // ── Mutation steps ────────────────────────────────────────────────────────
 
   visitTraversalMethod_addV_Empty(ctx: any): void {
diff --git a/gremlin-js/gremlin-javascript/lib/process/local/LocalExecutor.ts b/gremlin-js/gremlin-javascript/lib/process/local/LocalExecutor.ts
index 059b1ddc17..a37f25ebff 100644
--- a/gremlin-js/gremlin-javascript/lib/process/local/LocalExecutor.ts
+++ b/gremlin-js/gremlin-javascript/lib/process/local/LocalExecutor.ts
@@ -17,7 +17,7 @@
  * under the License.
  */
 
-import type { Arg, Pipeline, StepDescriptor } from './types.js';
+import type { Arg, Pipeline, StepDescriptor, ExecutionContext, RepeatSpec } from './types.js';
 import { defaultPasses, type OptimizationPass } from './passes.js';
 import { LocalExecutionError } from './LocalExecutionError.js';
 import { Graph, Vertex, Edge } from '../../structure/graph.js';
@@ -26,13 +26,21 @@ import {
   stepOut, stepIn, stepBoth, stepOutE, stepInE, stepBothE, stepOutV, stepInV, stepOtherV,
   stepHas, stepHasId, stepHasLabel, stepHasNot,
   stepId, stepLabel, stepValue, stepKey, stepValues, stepValueMap, stepElementMap,
-  stepPath,
+  stepPath, stepRepeat,
   stepLimit, stepRange, stepSkip, stepTail, stepOrder,
   stepAddV, stepAddEWithModulators, stepProperty,
 } from './steps.js';
 
 type StepFn = (source: Iterable<StreamItem>, args: Arg[], graph: Graph, trackPaths: boolean) => Generator<StreamItem>;
 
+/**
+ * A "parent" step that runs nested child pipelines (addE from/to, path projections,
+ * repeat bodies). It receives the ExecutionContext rather than reaching back into the
+ * executor through a bespoke escape, which is what lets new branching steps be added
+ * as ordinary registry entries instead of special cases in applyStep.
+ */
+type ParentStepFn = (source: Iterable<StreamItem>, args: Arg[], ctx: ExecutionContext) => Iterable<StreamItem>;
+
 /** Steps allowed inside a by(Traversal) projection on path() — single value extraction only. */
 const VALUE_EXTRACTION_STEPS = new Set<string>([
   'id', 'label', 'key', 'value', 'values', 'valueMap', 'elementMap',
@@ -52,21 +60,45 @@ export class LocalExecutor {
   execute(pipeline: Pipeline, graph: Graph): AsyncGenerator<any> {
     const optimized = this.passes.reduce((p, pass) => pass(p), pipeline);
     const folded = this.foldModulators(optimized);
-    const trackPaths = folded.some(s => s.name === 'path' || s.name === 'otherV');
-    const stream = this.buildChain(folded, graph, trackPaths);
+    const trackPaths = this.needsPaths(folded);
+    const ctx = this.makeContext(graph, trackPaths);
+    const stream = this.buildChain(folded, ctx);
     return this.toAsync(stream, trackPaths);
   }
 
   /**
-   * Folds 'from' and 'to' modulator steps that follow an 'addE' step into a
-   * composite descriptor so the addE executor has all modulator args available.
+   * Folds modulator steps into the steps they modify, then recurses into nested child
+   * pipelines (repeat bodies, until/emit conditions, addE from/to, path projections) so
+   * an inner order().by() or repeat() is folded too.
    */
   private foldModulators(pipeline: Pipeline): Pipeline {
+    const folded = this.foldLevel(pipeline);
+    for (const step of folded) {
+      if (step.name === 'repeat') {
+        const spec = step.args[0] as unknown as RepeatSpec;
+        spec.body = this.foldModulators(spec.body);
+        if (Array.isArray(spec.until)) spec.until = this.foldModulators(spec.until as Pipeline);
+        if (Array.isArray(spec.emit)) spec.emit = this.foldModulators(spec.emit as Pipeline);
+      } else {
+        for (let a = 0; a < step.args.length; a++) {
+          const arg = step.args[a];
+          if (isPipelineArg(arg)) {
+            step.args[a] = this.foldModulators(arg as unknown as Pipeline) as unknown as Arg;
+          }
+        }
+      }
+    }
+    return folded;
+  }
+
+  /** Folds the modulators at a single pipeline level (no recursion into child pipelines). */
+  private foldLevel(pipeline: Pipeline): Pipeline {
     const result: StepDescriptor[] = [];
     let i = 0;
     while (i < pipeline.length) {
       const step = pipeline[i];
       if (step.name === 'addE') {
+        // Fold the following from()/to() modulators into the addE descriptor.
         let fromArgs: Arg[] | null = null;
         let toArgs: Arg[] | null = null;
         let j = i + 1;
@@ -78,7 +110,7 @@ export class LocalExecutor {
         result.push({ name: 'addE', args: [step.args[0], fromArgs as unknown as Arg, toArgs as unknown as Arg] });
         i = j;
       } else if (step.name === 'order') {
-        // Fold a single following by() modulator into the order step args
+        // Fold a single following by() modulator into the order step args.
         const next = pipeline[i + 1];
         if (next?.name === 'by') {
           if (Array.isArray(next.args[0])) {
@@ -112,6 +144,26 @@ export class LocalExecutor {
         }
         result.push({ name: 'path', args: projections });
         i = j;
+      } else if (step.name === 'repeat') {
+        // repeat() with only trailing times()/until()/emit() modulators.
+        let m = i + 1;
+        while (m < pipeline.length && isRepeatModulator(pipeline[m].name)) m++;
+        result.push({ name: 'repeat', args: [buildRepeatSpec(step, [], pipeline.slice(i + 1, m)) as unknown as Arg] });
+        i = m;
+      } else if (isRepeatModulator(step.name)) {
+        // A leading run of times()/until()/emit() — these declare modulators on the
+        // repeat() that must immediately follow (e.g. until(c).repeat(b), emit().repeat(b)).
+        let k = i;
+        while (k < pipeline.length && isRepeatModulator(pipeline[k].name)) k++;
+        if (k >= pipeline.length || pipeline[k].name !== 'repeat') {
+          throw new LocalExecutionError(`'${step.name}' is only supported as a repeat() modulator in Tiny Gremlin`);
+        }
+        const leading = pipeline.slice(i, k);
+        let m = k + 1;
+        while (m < pipeline.length && isRepeatModulator(pipeline[m].name)) m++;
+        const trailing = pipeline.slice(k + 1, m);
+        result.push({ name: 'repeat', args: [buildRepeatSpec(pipeline[k], leading, trailing) as unknown as Arg] });
+        i = m;
       } else {
         result.push(step);
         i++;
@@ -120,79 +172,67 @@ export class LocalExecutor {
     return result;
   }
 
-  private buildChain(pipeline: Pipeline, graph: Graph, trackPaths: boolean): Iterable<StreamItem> {
-    if (pipeline.length === 0) return [];
+  /** True when paths must be tracked: a path()/otherV() anywhere top-level or inside a repeat body. */
+  private needsPaths(pipeline: Pipeline): boolean {
+    for (const step of pipeline) {
+      if (step.name === 'path' || step.name === 'otherV') return true;
+      if (step.name === 'repeat' && this.needsPaths((step.args[0] as unknown as RepeatSpec).body)) return true;
+    }
+    return false;
+  }
 
-    const [source, ...rest] = pipeline;
-    let stream: Iterable<StreamItem> = this.applySourceStep(source, graph, trackPaths);
+  /** Builds the ExecutionContext handed to parent steps for running their child pipelines. */
+  private makeContext(graph: Graph, trackPaths: boolean): ExecutionContext {
+    const ctx: ExecutionContext = {
+      graph,
+      trackPaths,
+      runBranch: (child, src) => {
+        let stream: Iterable<StreamItem> = src;
+        for (const step of child) stream = this.applyStep(step, stream, ctx);
+        return stream;
+      },
+      runProject: (child, object) => {
+        // child is a single value-extraction step (validated during folding).
+        const probe: ExecutionContext = { ...ctx, trackPaths: false };
+        for (const out of this.applyStep(child[0], [wrap(object, [], false)], probe)) {
+          return getValue(out, false);
+        }
+        return NON_PRODUCTIVE; // non-productive by(Traversal) filters the path traverser
+      },
+      runRooted: (child) => {
+        const probe: ExecutionContext = { ...ctx, trackPaths: false };
+        for (const item of this.buildChain(child, probe)) return item;
+        return null;
+      },
+    };
+    return ctx;
+  }
 
-    for (const step of rest) {
-      stream = this.applyStep(step, stream, graph, trackPaths);
-    }
+  private buildChain(pipeline: Pipeline, ctx: ExecutionContext): Iterable<StreamItem> {
+    if (pipeline.length === 0) return [];
+    const [source, ...rest] = pipeline;
+    let stream: Iterable<StreamItem> = this.applySourceStep(source, ctx);
+    for (const step of rest) stream = this.applyStep(step, stream, ctx);
     return stream;
   }
 
-  private applySourceStep(step: StepDescriptor, graph: Graph, trackPaths: boolean): Iterable<StreamItem> {
+  private applySourceStep(step: StepDescriptor, ctx: ExecutionContext): Iterable<StreamItem> {
     switch (step.name) {
-      case 'V': return sourceV(step.args, graph, trackPaths);
-      case 'E': return sourceE(step.args, graph, trackPaths);
-      case 'addV': return stepAddV([null], step.args, graph, trackPaths);
-      case 'addE': return this.applyAddE([null], step, graph, trackPaths);
+      case 'V': return sourceV(step.args, ctx.graph, ctx.trackPaths);
+      case 'E': return sourceE(step.args, ctx.graph, ctx.trackPaths);
+      case 'addV': return stepAddV([null], step.args, ctx.graph, ctx.trackPaths);
+      case 'addE': return PARENT_STEPS.get('addE')!([null], step.args, ctx);
       default:
         throw new LocalExecutionError(`'${step.name}' cannot be used as a source step`);
     }
   }
 
-  private applyStep(step: StepDescriptor, source: Iterable<StreamItem>, graph: Graph, trackPaths: boolean): Iterable<StreamItem> {
-    if (step.name === 'addE') return this.applyAddE(source, step, graph, trackPaths);
-    if (step.name === 'path') return this.applyPath(source, step, graph, trackPaths);
+  private applyStep(step: StepDescriptor, source: Iterable<StreamItem>, ctx: ExecutionContext): Iterable<StreamItem> {
+    const parent = PARENT_STEPS.get(step.name);
+    if (parent) return parent(source, step.args, ctx);
     const fn = STEP_REGISTRY.get(step.name);
     if (!fn) throw new LocalExecutionError(`No implementation for step '${step.name}'`);
-    return fn(source, step.args, graph, trackPaths);
-  }
-
-  /**
-   * Executes path() with its folded by() projections. Provides stepPath a callback that
-   * runs a single value-extraction sub-step against one path element, taking its first
-   * result (e.g. values('name') yields the first value).
-   */
-  private applyPath(
-    source: Iterable<StreamItem>,
-    step: StepDescriptor,
-    graph: Graph,
-    trackPaths: boolean,
-  ): Iterable<StreamItem> {
-    const runTraversal = (sub: Pipeline, object: any): any => {
-      const sub0 = sub[0];
-      const fn = STEP_REGISTRY.get(sub0.name);
-      if (!fn) throw new LocalExecutionError(`No implementation for step '${sub0.name}'`);
-      for (const out of fn([wrap(object, [], false)], sub0.args, graph, false)) {
-        return getValue(out, false);
-      }
-      return NON_PRODUCTIVE; // non-productive by(Traversal) filters the path traverser
-    };
-    return stepPath(source, step.args, trackPaths, runTraversal);
-  }
-
-  private applyAddE(
-    source: Iterable<StreamItem>,
-    step: StepDescriptor,
-    graph: Graph,
-    trackPaths: boolean,
-  ): Iterable<StreamItem> {
-    const label = step.args[0] as string;
-    const fromPipeline = step.args[1] as Arg[] | null;
-    const toPipeline = step.args[2] as Arg[] | null;
-    return stepAddEWithModulators(
-      source, label, fromPipeline, toPipeline, graph, trackPaths,
-      (subPipeline, g) => this.executeSubPipeline(subPipeline as unknown as Pipeline, g),
-    );
-  }
-
-  private executeSubPipeline(pipeline: Pipeline, graph: Graph): any {
-    const stream = this.buildChain(pipeline, graph, false);
-    for (const item of stream) return item;
-    return null;
+    return fn(source, step.args, ctx.graph, ctx.trackPaths);
   }
 
   private async *toAsync(stream: Iterable<StreamItem>, trackPaths: boolean): AsyncGenerator<any> {
@@ -205,6 +245,108 @@ export class LocalExecutor {
   }
 }
 
+// ── Modulator folding helpers ───────────────────────────────────────────────────
+
+function isRepeatModulator(name: string): boolean {
+  return name === 'times' || name === 'until' || name === 'emit';
+}
+
+/** True when an argument is itself a pipeline (a non-empty array of step descriptors). */
+function isPipelineArg(arg: unknown): boolean {
+  return Array.isArray(arg) && arg.length > 0 && arg[0] !== null
+    && typeof arg[0] === 'object' && 'name' in (arg[0] as object);
+}
+
+/** Mutation steps. Graph mutation inside a loop is undefined, so these are blocked within repeat(). */
+const MUTATION_STEPS = new Set<string>(['addV', 'addE', 'property']);
+
+/**
+ * Rejects mutation steps anywhere within a repeat() body or its until()/emit() conditions,
+ * recursing into nested child pipelines (including nested repeats), since mutating the graph
+ * while looping over it has no well-defined semantics.
+ */
+function assertNoMutationSteps(pipeline: Pipeline, context: string): void {
+  for (const step of pipeline) {
+    if (MUTATION_STEPS.has(step.name)) {
+      throw new LocalExecutionError(
+        `Mutation step '${step.name}()' is not supported inside ${context} in Tiny Gremlin`,
+      );
+    }
+    for (const arg of step.args) {
+      if (isPipelineArg(arg)) assertNoMutationSteps(arg as unknown as Pipeline, context);
+    }
+  }
+}
+
+/** Folds a repeat() step plus its leading/trailing modulators into a RepeatSpec. */
+function buildRepeatSpec(repeatStep: StepDescriptor, leading: StepDescriptor[], trailing: StepDescriptor[]): RepeatSpec {
+  const body = repeatStep.args[0];
+  if (!isPipelineArg(body)) {
+    throw new LocalExecutionError('repeat() with a loop label is not supported in Tiny Gremlin');
+  }
+  assertNoMutationSteps(body as unknown as Pipeline, 'a repeat() body');
+  const spec: RepeatSpec = {
+    body: body as unknown as Pipeline,
+    until: null, untilFirst: false,
+    emit: null, emitPresent: false, emitFirst: false,
+    times: null, timesFirst: false,
+  };
+  for (const mod of leading) applyRepeatModulator(spec, mod, true);
+  for (const mod of trailing) applyRepeatModulator(spec, mod, false);
+  return spec;
+}
+
+function applyRepeatModulator(spec: RepeatSpec, mod: StepDescriptor, declaredFirst: boolean): void {
+  if (mod.name === 'times') {
+    spec.times = mod.args[0] as number;
+    spec.timesFirst = declaredFirst;
+  } else if (mod.name === 'until') {
+    spec.until = requireTraversalCondition('until', mod.args[0]);
+    spec.untilFirst = declaredFirst;
+  } else if (mod.name === 'emit') {
+    spec.emitPresent = true;
+    spec.emit = mod.args.length ? requireTraversalCondition('emit', mod.args[0]) : null;
+    spec.emitFirst = declaredFirst;
+  }
+}
+
+/**
+ * Validates an until()/emit() condition. Only the anonymous-traversal form is supported.
+ * The predicate form (until(P)/emit(P)) maps to Java's Predicate<Traverser> lambda overload,
+ * which Tiny Gremlin cannot express faithfully, so it is rejected with a clear message.
+ */
+function requireTraversalCondition(stepName: string, arg: Arg | undefined): Pipeline | null {
+  if (arg == null) return null;
+  if (!isPipelineArg(arg)) {
+    throw new LocalExecutionError(
+      `${stepName}(Predicate) is not supported in Tiny Gremlin; use the traversal form, ` +
+      `e.g. ${stepName}(__.has(...))`,
+    );
+  }
+  const pipeline = arg as unknown as Pipeline;
+  assertNoMutationSteps(pipeline, `an ${stepName}() condition`);
+  return pipeline;
+}
+
+// ── Parent step registry ────────────────────────────────────────────────────────
+// Steps that run nested child pipelines. They receive the ExecutionContext and use
+// its runBranch/runProject/runRooted helpers, so adding a new branching step is a
+// registry entry rather than a special case in applyStep.
+
+const PARENT_STEPS = new Map<string, ParentStepFn>([
+  ['path', (source, args, ctx) => stepPath(source, args, ctx.trackPaths, (sub, obj) => ctx.runProject(sub, obj))],
+  ['addE', (source, args, ctx) => stepAddEWithModulators(
+    source,
+    args[0] as string,
+    args[1] as unknown as Arg[] | null,
+    args[2] as unknown as Arg[] | null,
+    ctx.graph,
+    ctx.trackPaths,
+    (sub) => ctx.runRooted(sub as unknown as Pipeline),
+  )],
+  ['repeat', (source, args, ctx) => stepRepeat(source, args[0] as unknown as RepeatSpec, ctx)],
+]);
+
 // ── Result copying ────────────────────────────────────────────────────────────
 
 /** Shallow-copies Vertex/Edge/Path so callers can mutate results without corrupting the graph. */
@@ -291,7 +433,7 @@ const STEP_REGISTRY = new Map<string, StepFn>([
   ['values',      stepValues],
   ['valueMap',    stepValueMap],
   ['elementMap',  stepElementMap],
-  // 'path' is dispatched via applyPath (needs the registry for by(Traversal) projections)
+  // 'path', 'addE' and 'repeat' are parent steps dispatched via PARENT_STEPS (they run child pipelines)
   ['limit',       stepLimit],
   ['range',       stepRange],
   ['skip',        stepSkip],
diff --git a/gremlin-js/gremlin-javascript/lib/process/local/passes.ts b/gremlin-js/gremlin-javascript/lib/process/local/passes.ts
index ce7635263a..be4088837b 100644
--- a/gremlin-js/gremlin-javascript/lib/process/local/passes.ts
+++ b/gremlin-js/gremlin-javascript/lib/process/local/passes.ts
@@ -31,6 +31,7 @@ const SUPPORTED_STEPS = new Set<string>([
   'path',
   'limit', 'range', 'skip', 'tail',
   'order', 'by',
+  'repeat', 'times', 'until', 'emit',
   'addV', 'addE', 'property',
   'from', 'to',
 ]);
diff --git a/gremlin-js/gremlin-javascript/lib/process/local/steps.ts b/gremlin-js/gremlin-javascript/lib/process/local/steps.ts
index ba2b04a5a0..164accf753 100644
--- a/gremlin-js/gremlin-javascript/lib/process/local/steps.ts
+++ b/gremlin-js/gremlin-javascript/lib/process/local/steps.ts
@@ -19,7 +19,7 @@
 
 import { P, TextP, t, direction } from '../traversal.js';
 import { Graph, Vertex, Edge, VertexProperty, Property, Path } from '../../structure/graph.js';
-import type { Arg, Pipeline } from './types.js';
+import type { Arg, Pipeline, ExecutionContext, RepeatSpec } from './types.js';
 
 // ── Traverser helpers ─────────────────────────────────────────────────────────
 
@@ -766,3 +766,79 @@ export function* stepProperty(
     yield item;
   }
 }
+
+// ── Repeat step ───────────────────────────────────────────────────────────────
+
+/**
+ * Executes repeat() with its folded times()/until()/emit() modulators against a
+ * stream. The loop is driven breadth-first over the whole frontier: at each loop
+ * depth, every surviving traverser advances in lockstep through one shared run of
+ * the body. until() is the exit condition (checked before the body when declared
+ * first — while — or after it otherwise — do-while), times() is a loop-count exit,
+ * and emit() is a side output that does not stop a traverser from continuing to loop.
+ *
+ * Driving the body over the entire frontier at once (rather than per input
+ * traverser) is what gives barrier steps inside the body — order().by(), and the
+ * same for a nested repeat() — their global semantics: order() sees every traverser
+ * at that loop position, not just the descendants of a single start. Because the
+ * frontier only ever holds traversers that have survived exactly `loops` iterations,
+ * the loop count is uniform across the frontier and the per-traverser times()/until()
+ * exits remain exact.
+ *
+ * Child pipelines (the body and any until()/emit() traversal conditions) are run
+ * through the ExecutionContext, so a repeat() body may itself contain order().by(),
+ * path() projections, or a nested repeat().
+ */
+export function* stepRepeat(
+  source: Iterable<StreamItem>,
+  spec: RepeatSpec,
+  ctx: ExecutionContext,
+): Generator<StreamItem> {
+  // until()/emit() conditions are always filter traversals here; the predicate form
+  // is rejected during folding (see requireTraversalCondition in LocalExecutor).
+  const conditionHolds = (cond: Pipeline, item: StreamItem): boolean => {
+    for (const _out of ctx.runBranch(cond, [item])) return true; // filter traversal: any output => true
+    return false;
+  };
+
+  const exitBeforeBody = (item: StreamItem, loops: number): boolean => {
+    if (spec.times != null && spec.timesFirst && loops >= spec.times) return true;
+    if (spec.untilFirst && spec.until != null && conditionHolds(spec.until, item)) return true;
+    return false;
+  };
+  const exitAfterBody = (item: StreamItem, loops: number): boolean => {
+    if (spec.times != null && !spec.timesFirst && loops >= spec.times) return true;
+    if (!spec.untilFirst && spec.until != null && conditionHolds(spec.until, item)) return true;
+    return false;
+  };
+  const shouldEmit = (item: StreamItem): boolean => {
+    if (!spec.emitPresent) return false;
+    if (spec.emit == null) return true; // bare emit() — emit every traverser
+    return conditionHolds(spec.emit, item);
+  };
+
+  let frontier: StreamItem[] = [...source];
+  let loops = 0;
+  while (frontier.length > 0) {
+    // While-phase: traversers that hit a before-body exit leave the loop now;
+    // the rest (optionally emitted ahead of the body) form the body's input.
+    const entering: StreamItem[] = [];
+    for (const item of frontier) {
+      if (exitBeforeBody(item, loops)) { yield item; continue; }
+      if (spec.emitFirst && shouldEmit(item)) yield item;
+      entering.push(item);
+    }
+    if (entering.length === 0) break;
+
+    // Run the body over the entire frontier in one pass so barriers see it all.
+    const nextLoops = loops + 1;
+    const nextFrontier: StreamItem[] = [];
+    for (const out of ctx.runBranch(spec.body, entering)) {
+      if (exitAfterBody(out, nextLoops)) { yield out; continue; }
+      if (!spec.emitFirst && shouldEmit(out)) yield out;
+      nextFrontier.push(out);
+    }
+    frontier = nextFrontier;
+    loops = nextLoops;
+  }
+}
diff --git a/gremlin-js/gremlin-javascript/lib/process/local/types.ts b/gremlin-js/gremlin-javascript/lib/process/local/types.ts
index a081f017d7..d1aee1bb83 100644
--- a/gremlin-js/gremlin-javascript/lib/process/local/types.ts
+++ b/gremlin-js/gremlin-javascript/lib/process/local/types.ts
@@ -18,6 +18,7 @@
  */
 
 import { P, TextP, EnumValue } from '../traversal.js';
+import type { Graph } from '../../structure/graph.js';
 
 /**
  * A single element flowing through a Tiny Gremlin local pipeline.
@@ -55,3 +56,43 @@ export interface StepDescriptor {
  * traversal or anonymous sub-traversal.
  */
 export type Pipeline = StepDescriptor[];
+
+/**
+ * The capabilities a "parent" step (one that runs nested child pipelines) needs
+ * from the executor. This is Tiny Gremlin's lightweight analog of Java's
+ * TraversalParent: rather than each parent step reaching back into the executor
+ * through bespoke escapes, the executor hands every parent step a context with
+ * three child-execution modes.
+ */
+export interface ExecutionContext {
+  graph: Graph;
+  trackPaths: boolean;
+  /** Apply a child pipeline as mid-traversal steps over an existing stream (flatMap per traverser). */
+  runBranch(child: Pipeline, source: Iterable<any>): Iterable<any>;
+  /** Run a single value-extraction child against one object, returning its first result or NON_PRODUCTIVE. */
+  runProject(child: Pipeline, object: any): any;
+  /** Run a child as a complete source-rooted pipeline (e.g. addE from/to), returning the first result. */
+  runRooted(child: Pipeline): any;
+}
+
+/**
+ * The folded form of a repeat() step and its times()/until()/emit() modulators.
+ * `untilFirst`/`emitFirst` record whether the modulator was declared before the
+ * repeat() (while/emit-before) or after it (do-while/emit-after). A null `emit`
+ * with `emitPresent` true is a bare emit() that always emits.
+ */
+export interface RepeatSpec {
+  body: Pipeline;
+  until: Pipeline | null;
+  untilFirst: boolean;
+  emit: Pipeline | null;
+  emitPresent: boolean;
+  emitFirst: boolean;
+  times: number | null;
+  /**
+   * Whether times() was declared before the repeat(). Like until(), position decides
+   * while vs. do-while: times(0).repeat(b) skips the body entirely (identity), while
+   * repeat(b).times(0) still runs the body once.
+   */
+  timesFirst: boolean;
+}
diff --git a/gremlin-js/gremlin-javascript/test/unit/tiny-gremlin-repeat-test.js b/gremlin-js/gremlin-javascript/test/unit/tiny-gremlin-repeat-test.js
new file mode 100644
index 0000000000..1708887b34
--- /dev/null
+++ b/gremlin-js/gremlin-javascript/test/unit/tiny-gremlin-repeat-test.js
@@ -0,0 +1,170 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+
+import { assert } from 'chai';
+import anon from '../../lib/process/anonymous-traversal.js';
+import { statics as __ } from '../../lib/process/graph-traversal.js';
+import { t, P } from '../../lib/process/traversal.js';
+import { buildModernGraph } from '../cucumber/local-fixtures.js';
+
+/**
+ * Unit coverage for repeat() and its times()/until()/emit() modulators in the
+ * Tiny Gremlin local executor, including modulator position (before vs. after the
+ * repeat), emit side-output semantics, and folding of modulators inside the loop body.
+ *
+ * Modern graph out-edges: marko(1)->{vadas(2),josh(4),lop(3)}, josh(4)->{ripple(5),lop(3)},
+ * peter(6)->{lop(3)}; vadas/lop/ripple have no out-edges.
+ */
+describe('Tiny Gremlin repeat()', () => {
+  let g;
+  const markoId = 1;
+
+  beforeEach(() => {
+    g = anon.traversal().with_(buildModernGraph().graph);
+  });
+
+  it('times(n) after repeat loops the body n times', async () => {
+    const names = await g.V(markoId).repeat(__.out()).times(2).values('name').toList();
+    // marko -> {vadas,josh,lop} -> only josh has out -> {ripple,lop}
+    assert.sameMembers(names, ['lop', 'ripple']);
+  });
+
+  it('times(n) before repeat has the same loop-count semantics', async () => {
+    const names = await g.V(markoId).times(2).repeat(__.out()).values('name').toList();
+    assert.sameMembers(names, ['lop', 'ripple']);
+  });
+
+  it('times(0) after repeat still runs the body once (do-while)', async () => {
+    const names = await g.V(markoId).repeat(__.out('created')).times(0).values('name').toList();
+    assert.deepEqual(names, ['lop']);
+  });
+
+  it('times(0) before repeat skips the body entirely (identity)', async () => {
+    const names = await g.V(markoId).times(0).repeat(__.out('created')).values('name').toList();
+    assert.deepEqual(names, ['marko']);
+  });
+
+  it('emit() after repeat side-outputs each post-body traverser', async () => {
+    const names = await g.V(markoId).repeat(__.out()).times(2).emit().values('name').toList();
+    // 1-hop set emitted after body 1 (vadas,josh,lop) plus the times-bounded 2-hop exit (ripple,lop)
+    assert.sameMembers(names, ['vadas', 'josh', 'lop', 'ripple', 'lop']);
+  });
+
+  it('until(traversal) after repeat is do-while (exit when the body output matches)', async () => {
+    const names = await g.V(markoId).repeat(__.out()).until(__.has('name', 'ripple')).values('name').toList();
+    // only the marko->josh->ripple branch reaches a vertex named ripple
+    assert.deepEqual(names, ['ripple']);
+  });
+
+  it('until(traversal) before repeat is while (condition tested before the body)', async () => {
+    // lop is already software, so while-semantics yields it without ever running out()
+    const lopId = 3;
+    const fromLop = await g.V(lopId).until(__.hasLabel('software')).repeat(__.out()).values('name').toList();
+    assert.deepEqual(fromLop, ['lop']);
+    // from marko (not software) the loop advances to the first software vertex on each path
+    const fromMarko = await g.V(markoId).until(__.hasLabel('software')).repeat(__.out()).values('name').toList();
+    assert.sameMembers(fromMarko, ['lop', 'ripple', 'lop']);
+  });
+
+  it('emit(traversal) before repeat side-outputs matching traversers ahead of the body', async () => {
+    const names = await g.V(markoId).emit(__.has(t.label, 'person')).repeat(__.out()).values('name').toList();
+    // person vertices reached while looping out(): marko, then vadas and josh
+    assert.sameMembers(names, ['marko', 'vadas', 'josh']);
+  });
+
+  it('supports a nested repeat() inside the loop body', async () => {
+    // body is a step followed by a bounded inner repeat (out().repeat(out()).times(1)),
+    // i.e. two hops expressed as embedded loops
+    const names = await g.V(markoId).repeat(__.out().repeat(__.out()).times(1)).times(1).values('name').toList();
+    assert.sameMembers(names, ['lop', 'ripple']);
+  });
+
+  it('folds modulators inside the repeat body (order().by())', async () => {
+    // order().by('name') inside the body proves the body sub-pipeline is folded too;
+    // an unfolded body would fail on the bare by() step.
+    const names = await g.V(markoId).repeat(__.out().order().by('name')).times(2).values('name').toList();
+    assert.sameMembers(names, ['lop', 'ripple']);
+  });
+
+  it('order() inside repeat() barriers globally over the whole loop frontier', async () => {
+    // The body runs over the entire frontier at once, so order() sorts all of g.V().both()
+    // together (global), not within each start vertex's neighbours (which would be unsorted
+    // once concatenated). 12 both()-endpoints, globally ordered by name.
+    const names = await g.V().repeat(__.both().order().by('name')).times(1).values('name').toList();
+    assert.deepEqual(names, [
+      'josh', 'josh', 'josh', 'lop', 'lop', 'lop', 'marko', 'marko', 'marko', 'peter', 'ripple', 'vadas',
+    ]);
+  });
+
+  it('preserves global order() semantics through a nested repeat()', async () => {
+    // Same globalness, with the order() buried one repeat() deeper:
+    // repeat(both().repeat(order().by(name)).times(1)).times(1).
+    const names = await g.V()
+      .repeat(__.both().repeat(__.order().by('name')).times(1)).times(1)
+      .values('name').toList();
+    assert.deepEqual(names, [
+      'josh', 'josh', 'josh', 'lop', 'lop', 'lop', 'marko', 'marko', 'marko', 'peter', 'ripple', 'vadas',
+    ]);
+  });
+
+  it('rejects repeat() with a loop label (no step labels in Tiny Gremlin)', async () => {
+    try {
+      await g.V(markoId).repeat('a', __.out()).times(2).values('name').toList();
+      assert.fail('expected a LocalExecutionError for a labeled repeat()');
+    } catch (err) {
+      assert.match(err.message, /loop label is not supported/);
+    }
+  });
+
+  it('rejects the predicate form of until() (only the traversal form is supported)', async () => {
+    try {
+      await g.V(markoId).repeat(__.out()).until(P.gt(100)).values('name').toList();
+      assert.fail('expected a LocalExecutionError for until(P)');
+    } catch (err) {
+      assert.match(err.message, /until\(Predicate\) is not supported/);
+    }
+  });
+
+  it('rejects a mutation step inside a repeat() body', async () => {
+    try {
+      await g.V(markoId).repeat(__.addV('x')).times(1).toList();
+      assert.fail('expected a LocalExecutionError for addV() inside repeat()');
+    } catch (err) {
+      assert.match(err.message, /Mutation step 'addV\(\)' is not supported inside a repeat\(\) body/);
+    }
+  });
+
+  it('rejects a mutation step inside an until() condition', async () => {
+    try {
+      await g.V(markoId).repeat(__.out()).until(__.property('k', 'v')).toList();
+      assert.fail('expected a LocalExecutionError for property() inside until()');
+    } catch (err) {
+      assert.match(err.message, /Mutation step 'property\(\)' is not supported inside an until\(\) condition/);
+    }
+  });
+
+  it('rejects a mutation step nested inside an inner repeat() body', async () => {
+    try {
+      await g.V(markoId).repeat(__.repeat(__.addE('x')).times(1)).times(1).toList();
+      assert.fail('expected a LocalExecutionError for addE() nested in repeat()');
+    } catch (err) {
+      assert.match(err.message, /Mutation step 'addE\(\)' is not supported inside a repeat\(\) body/);
+    }
+  });
+});
diff --git a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/branch/Repeat.feature b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/branch/Repeat.feature
index 60c17a6008..a5652a312b 100644
--- a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/branch/Repeat.feature
+++ b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/branch/Repeat.feature
@@ -18,6 +18,7 @@
 @StepClassBranch @StepRepeat
 Feature: Step - repeat()
 
+  @TinyGremlin
   Scenario: g_V_repeatXoutX_timesX2X_emit_path
     Given the modern graph
     And the traversal of
@@ -36,6 +37,7 @@ Feature: Step - repeat()
       | p[v[josh],v[lop]] |
       | p[v[peter],v[lop]] |
 
+  @TinyGremlin
   Scenario: g_V_repeatXoutX_timesX2X_repeatXinX_timesX2X_name
     Given the modern graph
     And the traversal of
@@ -48,7 +50,7 @@ Feature: Step - repeat()
       | marko |
       | marko |
 
-  @GraphComputerVerificationReferenceOnly
+  @GraphComputerVerificationReferenceOnly @TinyGremlin
   Scenario: g_V_repeatXoutE_inVX_timesX2X_path_by_name_by_label
     Given the modern graph
     And the traversal of
@@ -61,6 +63,7 @@ Feature: Step - repeat()
       | p[marko,knows,josh,created,lop] |
       | p[marko,knows,josh,created,ripple] |
 
+  @TinyGremlin
   Scenario: g_V_repeatXoutX_timesX2X
     Given the modern graph
     And the traversal of
@@ -73,6 +76,7 @@ Feature: Step - repeat()
       | v[ripple] |
       | v[lop] |
 
+  @TinyGremlin
   Scenario: g_V_repeatXoutX_timesX2X_emit
     Given the modern graph
     And the traversal of
@@ -88,6 +92,7 @@ Feature: Step - repeat()
       | v[vadas] |
     And the result should have a count of 8
 
+  @TinyGremlin
   Scenario: g_VX1X_timesX2X_repeatXoutX_name
     Given the modern graph
     And using the parameter vid1 defined as "v[marko].id"
@@ -101,6 +106,7 @@ Feature: Step - repeat()
       | ripple |
       | lop |
 
+  @TinyGremlin
   Scenario: g_V_emit_timesX2X_repeatXoutX_path
     Given the modern graph
     And the traversal of
@@ -125,6 +131,7 @@ Feature: Step - repeat()
       | p[v[peter]] |
       | p[v[peter],v[lop]] |
 
+  @TinyGremlin
   Scenario: g_V_emit_repeatXoutX_timesX2X_path
     Given the modern graph
     And the traversal of
@@ -149,6 +156,7 @@ Feature: Step - repeat()
       | p[v[peter]] |
       | p[v[peter],v[lop]] |
 
+  @TinyGremlin
   Scenario: g_VX1X_emitXhasXlabel_personXX_repeatXoutX_name
     Given the modern graph
     And using the parameter vid1 defined as "v[marko].id"
@@ -224,7 +232,7 @@ Feature: Step - repeat()
       | result |
       | p[marko,knows,josh,created,ripple]  |
 
-  @GraphComputerVerificationReferenceOnly
+  @GraphComputerVerificationReferenceOnly @TinyGremlin
   Scenario: g_V_hasXloop_name_loopX_repeatXinX_timesX5X_path_by_name
     Given the sink graph
     And the traversal of
@@ -236,7 +244,7 @@ Feature: Step - repeat()
       | result |
       | p[loop,loop,loop,loop,loop,loop] |
 
-  @GraphComputerVerificationReferenceOnly
+  @GraphComputerVerificationReferenceOnly @TinyGremlin
   Scenario: g_V_repeatXout_repeatXout_order_byXname_descXX_timesX1XX_timesX1X_limitX1X_path_byXnameX
       Given the modern graph
       And the traversal of
@@ -248,7 +256,7 @@ Feature: Step - repeat()
           | result |
           | p[marko,josh,ripple] |
 
-  @GraphComputerVerificationReferenceOnly
+  @GraphComputerVerificationReferenceOnly @TinyGremlin
   Scenario: g_V_repeatXoutXknowsXX_untilXrepeatXoutXcreatedXX_emitXhasXname_lopXXX_path_byXnameX
     Given the modern graph
     And the traversal of
@@ -260,6 +268,7 @@ Feature: Step - repeat()
       | result |
       | p[marko,josh] |
 
+  @TinyGremlin
   Scenario: g_V_repeatXrepeatXout_createdXX_untilXhasXname_rippleXXXemit_lang
     Given the modern graph
     And the traversal of
@@ -375,6 +384,7 @@ Feature: Step - repeat()
     When iterated to list
     Then the traversal will raise an error with message containing text of "The repeat()-traversal was not defined"
 
+  @TinyGremlin
   Scenario: g_V_hasXperson_name_markoX_repeatXoutXcreatedXX_timesX1X_name
     Given the modern graph
     And the traversal of
@@ -386,6 +396,7 @@ Feature: Step - repeat()
       | result |
       | lop |
 
+  @TinyGremlin
   Scenario: g_V_hasXperson_name_markoX_repeatXoutXcreatedXX_timesX0X_name
     Given the modern graph
     And the traversal of
@@ -397,6 +408,7 @@ Feature: Step - repeat()
       | result |
       | lop |
 
+  @TinyGremlin
   Scenario: g_V_hasXperson_name_markoX_timesX1X_repeatXoutXcreatedXX_name
     Given the modern graph
     And the traversal of
@@ -408,6 +420,7 @@ Feature: Step - repeat()
       | result |
       | lop |
 
+  @TinyGremlin
   Scenario: g_V_hasXperson_name_markoX_timesX0X_repeatXoutXcreatedXX_name
     Given the modern graph
     And the traversal of
@@ -508,6 +521,7 @@ Feature: Step - repeat()
       | p[v[marko],v[lop],v[josh]] |
 
   # Nested repeat should maintain globalness
+  @TinyGremlin
   Scenario: g_V_repeatXboth_repeatXorder_byXnameXX_timesX1XX_timesX1X
     Given the modern graph
     And the traversal of


(tinkerpop) 01/06: Add Tiny Gremlin — local in-process traversal for gremlin-javascript

Posted by sp...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

spmallette pushed a commit to branch jsprocess
in repository https://gitbox.apache.org/repos/asf/tinkerpop.git

commit fb51129928845af9984923218788d01cc8a89297
Author: Stephen Mallette <st...@amazon.com>
AuthorDate: Tue Jun 9 12:42:19 2026 -0400

    Add Tiny Gremlin — local in-process traversal for gremlin-javascript
    
    Tiny Gremlin is a defined 33-step subset of Gremlin that executes locally
    against a detached Graph object without a server. Primary use case: call
    subgraph() on a remote graph and navigate/filter the result in-process.
    Secondary use case: build small graphs manually with addV()/addE().
    
    Implementation (gremlin-javascript):
    - LocalGraphConnection: RemoteConnection adapter that accepts a Graph via
      with_(graph), parses GremlinLang with the ANTLR parser, and executes locally
    - ExecuteVisitor: walks the ANTLR parse tree into a StepDescriptor pipeline
    - LocalExecutor: chains lazy generators per step, with optimization passes
      (validation, hasId folding) and modulator folding (from/to into addE,
      by into order)
    - steps.ts: full Gremlin Orderability and Comparability semantics
    - passes.ts: validation and hasId-folding optimization passes
    - Graph.ts: outEdges/inEdges adjacency maps for O(1) neighbor lookup
    - Traversal: forEach() and reduce() terminal methods
    - @TinyGremlin Gherkin tag on existing scenarios for cross-language coverage
    
    Documentation:
    - docs/src/dev/provider/tiny-gremlin.asciidoc: full Tiny Gremlin semantics
    - docs/src/reference/the-traversal.asciidoc: Tiny Gremlin reference section
      and Tiny Gremlin Semantics links in Additional References for all 22
      covered step sections
    - docs/src/dev/provider/gremlin-semantics.asciidoc: placeholder sections
      for steps referenced by Tiny Gremlin
    
    Assisted-by: Claude Code:claude-sonnet-4-6
---
 CHANGELOG.asciidoc                                 |   1 +
 docs/src/dev/provider/gremlin-semantics.asciidoc   | 140 ++++
 docs/src/dev/provider/index.asciidoc               |   2 +
 docs/src/dev/provider/tiny-gremlin.asciidoc        | 385 +++++++++++
 docs/src/reference/the-traversal.asciidoc          | 134 +++-
 docs/static/images/tiny-gremlin.png                | Bin 0 -> 85492 bytes
 .../Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs |   5 +
 gremlin-go/driver/cucumber/gremlin.go              |   5 +
 .../lib/driver/local-graph-connection.ts           | 120 ++++
 gremlin-js/gremlin-javascript/lib/index.ts         |   4 +
 .../lib/language/executor/ExecuteVisitor.ts        | 708 ++++++++++++++++++++
 .../lib/process/anonymous-traversal.ts             |  13 +-
 .../lib/process/local/LocalExecutionError.ts       |  29 +
 .../lib/process/local/LocalExecutor.ts             | 248 +++++++
 .../gremlin-javascript/lib/process/local/passes.ts |  87 +++
 .../gremlin-javascript/lib/process/local/steps.ts  | 730 +++++++++++++++++++++
 .../gremlin-javascript/lib/process/local/types.ts  |  57 ++
 .../gremlin-javascript/lib/process/traversal.ts    |  28 +
 .../gremlin-javascript/lib/structure/graph.ts      |  16 +
 .../io/binary/internals/GraphSerializer.js         |   1 +
 gremlin-js/gremlin-javascript/package.json         |   1 +
 .../gremlin-javascript/test/cucumber/gremlin.js    |   7 +-
 .../test/cucumber/local-fixtures.ts                | 231 +++++++
 .../test/cucumber/local-world.ts                   |  84 +++
 .../src/main/python/tests/feature/gremlin.py       |   5 +
 .../gremlin/language/translator/translations.json  |  85 +++
 .../gremlin/test/features/filter/Has.feature       |  42 +-
 .../gremlin/test/features/filter/HasId.feature     |  12 +
 .../gremlin/test/features/filter/HasLabel.feature  |  15 +-
 .../gremlin/test/features/filter/HasNot.feature    |  17 +-
 .../gremlin/test/features/filter/Range.feature     |  14 +
 .../gremlin/test/features/filter/Tail.feature      |   4 +
 .../gremlin/test/features/map/AddEdge.feature      |   5 +-
 .../gremlin/test/features/map/AddVertex.feature    |  33 +-
 .../gremlin/test/features/map/Edge.feature         |  24 +-
 .../gremlin/test/features/map/ElementMap.feature   |   5 +-
 .../gremlin/test/features/map/Order.feature        |  59 +-
 .../gremlin/test/features/map/Path.feature         |  17 +-
 .../gremlin/test/features/map/Properties.feature   |   1 +
 .../gremlin/test/features/map/ValueMap.feature     |   6 +
 .../gremlin/test/features/map/Vertex.feature       |  31 +-
 .../test/features/semantics/Orderability.feature   |   1 +
 42 files changed, 3365 insertions(+), 47 deletions(-)

diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc
index 963ad54f5c..b5c70c2d21 100644
--- a/CHANGELOG.asciidoc
+++ b/CHANGELOG.asciidoc
@@ -130,6 +130,7 @@ image::https://raw.githubusercontent.com/apache/tinkerpop/master/docs/static/ima
 * Added the `gql-gremlin` module containing the TinkerGQL engine, a `MATCH`-pattern executor implementing a deliberate minimal subset of ISO GQL, usable by any graph provider.
 * Added `Graph.countVerticesByLabel(String)`, `Graph.countEdgesByLabel(String)`, and the `Graph.Index` nested interface as performance-hint extension points for the TinkerGQL engine.
 * Added GQL support to TinkerGraph with `gql-gremlin`,
+* Added Tiny Gremlin local execution to `gremlin-javascript`: `traversal().with_(graph)` now executes a defined subset of Gremlin steps (navigation, filtering, value extraction, path tracking, range, ordering, and local mutation) against an in-memory `Graph` without a remote server. The `Graph` adjacency index (`outEdges`, `inEdges`) enables O(1) navigation. New `forEach(fn)` and `reduce(fn, seed)` terminal methods on `Traversal` support result processing with native JavaScript functions [...]
 
 [[release-4-0-0-beta-2]]
 === TinkerPop 4.0.0-beta.2 (April 1, 2026)
diff --git a/docs/src/dev/provider/gremlin-semantics.asciidoc b/docs/src/dev/provider/gremlin-semantics.asciidoc
index 8b25408ff2..f551caf060 100644
--- a/docs/src/dev/provider/gremlin-semantics.asciidoc
+++ b/docs/src/dev/provider/gremlin-semantics.asciidoc
@@ -504,6 +504,146 @@ and the corresponding reference documentation. When a step has no entries for `A
 the field is still present with `None` as its body. When adding a new step or revising an existing one, follow the
 pattern from a neighboring entry rather than introducing a new structure.
 
+[[e-step]]
+=== E()
+
+To be written...
+
+[[v-step]]
+=== V()
+
+To be written...
+
+[[elementmap-step]]
+=== elementMap()
+
+To be written...
+
+[[has-step]]
+=== has()
+
+To be written...
+
+[[hasid-step]]
+=== hasId()
+
+To be written...
+
+[[haskey-step]]
+=== hasKey()
+
+To be written...
+
+[[haslabel-step]]
+=== hasLabel()
+
+To be written...
+
+[[hasnot-step]]
+=== hasNot()
+
+To be written...
+
+[[hasvalue-step]]
+=== hasValue()
+
+To be written...
+
+[[id-step]]
+=== id()
+
+To be written...
+
+[[ine-step]]
+=== inE()
+
+To be written...
+
+[[inv-step]]
+=== inV()
+
+To be written...
+
+[[in-step]]
+=== in()
+
+To be written...
+
+[[key-step]]
+=== key()
+
+To be written...
+
+[[label-step]]
+=== label()
+
+To be written...
+
+[[limit-step]]
+=== limit()
+
+To be written...
+
+[[order-step]]
+=== order()
+
+To be written...
+
+[[otherv-step]]
+=== otherV()
+
+To be written...
+
+[[out-step]]
+=== out()
+
+To be written...
+
+[[oute-step]]
+=== outE()
+
+To be written...
+
+[[outv-step]]
+=== outV()
+
+To be written...
+
+[[path-step]]
+=== path()
+
+To be written...
+
+[[property-step]]
+=== property()
+
+To be written...
+
+[[range-step]]
+=== range()
+
+To be written...
+
+[[skip-step]]
+=== skip()
+
+To be written...
+
+[[tail-step]]
+=== tail()
+
+To be written...
+
+[[value-step]]
+=== value()
+
+To be written...
+
+[[values-step]]
+=== values()
+
+To be written...
+
 [[adde-step]]
 === addE()
 
diff --git a/docs/src/dev/provider/index.asciidoc b/docs/src/dev/provider/index.asciidoc
index a09c67b915..c55bdc0033 100644
--- a/docs/src/dev/provider/index.asciidoc
+++ b/docs/src/dev/provider/index.asciidoc
@@ -1903,4 +1903,6 @@ in use with the Gremlin Console plugin host.  Simply instantiate and return a `R
 
 include::gremlin-semantics.asciidoc[]
 
+include::tiny-gremlin.asciidoc[]
+
 include::policies.asciidoc[]
\ No newline at end of file
diff --git a/docs/src/dev/provider/tiny-gremlin.asciidoc b/docs/src/dev/provider/tiny-gremlin.asciidoc
new file mode 100644
index 0000000000..2dd151598a
--- /dev/null
+++ b/docs/src/dev/provider/tiny-gremlin.asciidoc
@@ -0,0 +1,385 @@
+////
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+////
+
+[[tiny-gremlin]]
+= Tiny Gremlin
+
+image:tiny-gremlin.png[]
+
+== Introduction
+
+*Tiny Gremlin* is a defined subset of the Gremlin traversal language intended for local, in-process graph traversal
+against small, detached `Graph` instances. A common use case would be to take the result of a `subgraph()` call and then
+locally traverse it with Tiny Gremlin. It specifies which steps must be supported, the precise semantics of each step
+within the local execution model, and any deviations from the full Gremlin standard that implementors must be aware of.
+
+=== Motivation
+
+The primary use case for Tiny Gremlin is post-processing of subgraphs received from a remote graph server without
+requiring an additional network round-trip. A user retrieves a subgraph, holds it in memory as a `Graph` object, and
+then navigates and filters it locally using familiar Gremlin syntax. A secondary use case is constructing small graphs
+entirely in process for lightweight analysis or testing, using `addV()`, `addE()`, and `property()`.
+
+Because the graph is small and entirely in memory, features that are expensive or architecture-dependent in a full
+implementation, such as `repeat()`, distributed OLAP steps, aggregation barriers, and strategy-based optimizations,
+are deliberately excluded. The result is a small, self-contained, predictable execution model.
+
+=== Supported Steps
+
+Tiny Gremlin supports exactly the 33 steps listed below, grouped by category. No other step may be used in a Tiny
+Gremlin traversal; doing so results in an error before any graph data is accessed.
+
+[width="100%",options="header"]
+|===
+|Category |Steps
+|Source |`V()`, `E()`
+|Navigation |`out()`, `in()`, `both()`, `outE()`, `inE()`, `bothE()`, `outV()`, `inV()`, `otherV()`
+|Filter |`has()`, `hasId()`, `hasLabel()`, `hasNot()`
+|Value Extraction |`id()`, `label()`, `values()`, `valueMap()`, `elementMap()`, `value()`, `key()`
+|Path |`path()`
+|Range |`limit()`, `range()`, `skip()`, `tail()`
+|Ordering |`order()`
+|Mutation |`addV()`, `addE()`, `property()`
+|Modulators |`from()`, `to()`
+|===
+
+=== Terminal Methods
+
+Tiny Gremlin must support the standard `toList()`, `next()`, and `iterate()` terminal methods, but should offer
+language-specific terminals designed to help with stream processing ergonomics. For example, in Javascript, TinkerPop
+offers:
+
+* `forEach(fn)` — calls `fn` for each result element; returns `Promise<void>`.
+* `reduce(fn, seed)` — folds all results into a single value using `fn` and an initial `seed`; returns `Promise<T>`.
+
+Since Tiny Gremlin lacks steps needed for data transformation and filtering, these added steps will allow users to
+express post-processing functions on the result set.
+
+== Step Semantics
+
+The following sub-sections describe each supported step, its relationship to the full Gremlin Semantics definition,
+and any limitations or behavioral differences that apply specifically to Tiny Gremlin.
+
+=== addE()
+
+*Gremlin Semantics reference:* <<adde-step,addE()>>
+
+Creates a new edge in the local `Graph` between vertices specified by `from()` and `to()` modulators.
+
+*Tiny Gremlin limitations:*
+
+* Only the `addE(String edgeLabel)` form is supported. The `addE(Traversal)` label-traversal form is not supported
+  because the traversal would need to reference prior labeled elements via `as()` and `select()`, neither of which
+  is in the Tiny Gremlin step set.
+* `from()` and `to()` must use the anonymous traversal form (e.g. `from(__.V(id))`). The step-label form
+  (`from("stepLabel")`) is not supported because `as()` is not in the Tiny Gremlin step set. The GLV convenience
+  sugar `from_(vertexObject)` does not work because the GLV converts the `Vertex` to its raw ID before
+  serialization, producing `from(id)` which is not a valid grammar form and results in a parse error.
+* The vertex referenced by `from()` or `to()` must exist in the local `Graph` at execution time. If the ID is not
+  found, an error is raised. No vertex is created implicitly.
+
+=== addV()
+
+*Gremlin Semantics reference:* <<addv-step,addV()>>
+
+Creates a new vertex in the local `Graph`.
+
+*Tiny Gremlin limitations:*
+
+* No cardinality-aware property form (`property(Cardinality, key, value)`) is supported; only the simple
+  `property(key, value)` form applies to vertices created by `addV()`.
+* Changes are local only; the remote graph is not modified.
+
+=== both()
+
+*Gremlin Semantics reference:* <<both-step,both()>>
+
+Maps a vertex to its adjacent vertices, traversing both incoming and outgoing edges.
+
+=== bothE()
+
+*Gremlin Semantics reference:* <<bothe-step,bothE()>>
+
+Maps a vertex to its incident edges, both incoming and outgoing.
+
+=== E()
+
+*Gremlin Semantics reference:* <<e-step,E()>>
+
+Produces edges from the local graph. When called with no arguments, all edges are produced. When called with one or
+more IDs, only matching edges are produced. A single list argument is also accepted.
+
+=== elementMap()
+
+*Gremlin Semantics reference:* <<elementmap-step,elementMap()>>
+
+Maps an element to a `Map` containing its `T.id`, `T.label`, and property key-value pairs. For edges,
+`Direction.OUT` and `Direction.IN` keys are included as stub maps containing `T.id` and `T.label` of the adjacent
+vertices.
+
+*Tiny Gremlin limitations:*
+
+* `by()` modulation is not supported.
+
+=== has()
+
+*Gremlin Semantics reference:* <<has-step,has()>>
+
+Filters elements by property existence, equality, or predicate.
+
+Supported forms:
+
+* `has(key)` — element has the property.
+* `has(key, value)` — element has the property with the given value.
+* `has(key, P)` — element has the property satisfying the predicate.
+* `has(label, key, value)` — element has the given label and the property with the given value.
+* `has(label, key, P)` — element has the given label and the property satisfying the predicate.
+
+Supported `P` predicates: `eq`, `neq`, `lt`, `lte`, `gt`, `gte`, `between`, `inside`, `outside`, `within`,
+`without`, `not`. Supported `TextP` predicates: `containing`, `notContaining`, `startingWith`, `notStartingWith`,
+`endingWith`, `notEndingWith`, `regex`, `notRegex`.
+
+The ordering predicates (`lt`, `lte`, `gt`, `gte`, `between`, `inside`, `outside`) follow the
+<<gremlin-semantics-concepts,Gremlin Comparability semantics>>: cross-type comparisons and comparisons involving
+`NaN` return `false` (the comparability ERROR result).
+
+*Tiny Gremlin limitations:*
+
+* Only `T.id` and `T.label` are supported as key tokens. `T.key` and `T.value` are not supported because
+  `properties()` is not in the Tiny Gremlin step set and there is no other way to produce `Property` traversers.
+
+=== hasId()
+
+*Gremlin Semantics reference:* <<hasid-step,hasId()>>
+
+Filters elements by their ID.
+
+*Tiny Gremlin limitations:*
+
+* `hasId([1, 2, 3])` — a single list argument — is unrolled and treated as `hasId(1, 2, 3)`, consistent with
+  `V([1,2,3])` semantics.
+* When more than one argument is provided, each argument is treated as a distinct ID value. A list in that position
+  is compared as-is and will not match any scalar ID (e.g. `hasId(1, [2, 3])` matches only id `1`). This
+  intentionally differs from the standard `hasId()` implementation which unrolls lists in all positions (see
+  TINKERPOP-2863).
+
+=== hasKey()
+
+*Gremlin Semantics reference:* <<haskey-step,hasKey()>>
+
+Filters `Property` traversers by key. Operates on the output of `properties()`.
+
+=== hasLabel()
+
+*Gremlin Semantics reference:* <<haslabel-step,hasLabel()>>
+
+Filters elements by label.
+
+=== hasNot()
+
+*Gremlin Semantics reference:* <<hasnot-step,hasNot()>>
+
+Filters elements by the absence of a property.
+
+=== hasValue()
+
+*Gremlin Semantics reference:* <<hasvalue-step,hasValue()>>
+
+Filters `Property` traversers by value. Operates on the output of `properties()`.
+
+=== id()
+
+*Gremlin Semantics reference:* <<id-step,id()>>
+
+Maps an element to its ID.
+
+=== in()
+
+*Gremlin Semantics reference:* <<in-step,in()>>
+
+Maps a vertex to its adjacent incoming vertices.
+
+=== inE()
+
+*Gremlin Semantics reference:* <<ine-step,inE()>>
+
+Maps a vertex to its incoming edges.
+
+=== inV()
+
+*Gremlin Semantics reference:* <<inv-step,inV()>>
+
+Maps an edge to its incoming vertex (the head of the arrow).
+
+=== key()
+
+*Gremlin Semantics reference:* <<key-step,key()>>
+
+Maps a `Property` traverser to its key.
+
+=== label()
+
+*Gremlin Semantics reference:* <<label-step,label()>>
+
+Maps an element to its label.
+
+=== limit()
+
+*Gremlin Semantics reference:* <<limit-step,limit()>>
+
+Truncates the result stream to at most `n` elements.
+
+*Tiny Gremlin limitations:*
+
+* The `Scope` form is not supported and will raise an error.
+
+=== order()
+
+*Gremlin Semantics reference:* <<order-step,order()>>
+
+Sorts all elements.
+
+Supports a single `by()` modulator with the following forms:
+
+* `by(String key)` — sort by property value ascending.
+* `by(String key, Order)` — sort by property value with explicit direction (`Order.asc` or `Order.desc`).
+* `by(T.id)` / `by(T.id, Order)` and `by(T.label)` / `by(T.label, Order)` — sort by the element's ID or label
+  with optional direction.
+* `by(Order)` — sort by natural element value with explicit direction.
+* `by()` — natural ascending sort (same as omitting `by()`).
+
+*Tiny Gremlin limitations:*
+
+* Only a single `by()` modulator is supported per `order()`.
+* Ordering follows the <<gremlin-semantics-orderability,Gremlin Orderability semantics>>, providing a total order
+  across all types: `null` < `Boolean` < `Number` < `Date` < `String` < `Vertex` < `Edge` < `VertexProperty`
+  < `Property` < `Path` < `Set` < `List` < `Map` < `Unknown`. `NaN` is ordered after `+Infinity` within the
+  numeric type space.
+* The `Scope` form (`order(Scope)`) is not supported and will raise an error.
+* `by(Traversal)`, `by(Traversal, Order)`, and `by(Function)` are not supported and will raise an error.
+
+=== otherV()
+
+*Gremlin Semantics reference:* <<otherv-step,otherV()>>
+
+Maps an edge to the vertex that was not the vertex traversed from.
+
+=== out()
+
+*Gremlin Semantics reference:* <<out-step,out()>>
+
+Maps a vertex to its adjacent outgoing vertices.
+
+=== outE()
+
+*Gremlin Semantics reference:* <<oute-step,outE()>>
+
+Maps a vertex to its outgoing edges.
+
+=== outV()
+
+*Gremlin Semantics reference:* <<outv-step,outV()>>
+
+Maps an edge to its outgoing vertex (the tail of the arrow).
+
+=== path()
+
+*Gremlin Semantics reference:* <<path-step,path()>>
+
+Maps the current traverser to the `Path` of elements it has visited.
+
+*Tiny Gremlin limitations:*
+
+* `by()` modulation is not supported. Path objects always contain the raw elements visited.
+* `from()` and `to()` selection within a path is not supported because `as()` step labels are excluded from Tiny
+  Gremlin.
+
+=== property()
+
+*Gremlin Semantics reference:* <<property-step,property()>>
+
+Sets a property on the current element.
+
+*Tiny Gremlin limitations:*
+
+* Only the `property(key, value)` two-argument form is supported. The cardinality form
+  `property(Cardinality, key, value)` and the map form `property(map)` are not supported.
+* For vertices, an existing property with the same key is replaced (single cardinality semantics regardless of graph
+  configuration).
+
+=== range()
+
+*Gremlin Semantics reference:* <<range-step,range()>>
+
+Emits elements at positions `[low, high)` in the stream, zero-indexed.
+
+*Tiny Gremlin limitations:*
+
+* The `Scope` form is not supported and will raise an error.
+
+=== skip()
+
+*Gremlin Semantics reference:* <<skip-step,skip()>>
+
+Discards the first `n` elements of the stream.
+
+*Tiny Gremlin limitations:*
+
+* The `Scope` form is not supported and will raise an error.
+
+=== tail()
+
+*Gremlin Semantics reference:* <<tail-step,tail()>>
+
+Emits the last `n` elements of the stream. This is a barrier step.
+
+*Tiny Gremlin limitations:*
+
+* The `Scope` form is not supported and will raise an error.
+
+=== V()
+
+*Gremlin Semantics reference:* <<v-step,V()>>
+
+Produces vertices from the local graph. When called with no arguments, all vertices are produced. When called with one
+or more IDs, only matching vertices are produced. A single list argument is also accepted.
+
+=== value()
+
+*Gremlin Semantics reference:* <<value-step,value()>>
+
+Maps a `Property` traverser to its value.
+
+=== valueMap()
+
+*Gremlin Semantics reference:* <<valueMap-step,valueMap()>>
+
+Maps an element to a `Map` of property keys to lists of property values.
+
+*Tiny Gremlin limitations:*
+
+* `by()` modulation is not supported.
+* `with(WithOptions.tokens)` configuration is not supported; use `valueMap(true)` instead.
+
+=== values()
+
+*Gremlin Semantics reference:* <<values-step,values()>>
+
+Maps an element to the values of its properties.
+
diff --git a/docs/src/reference/the-traversal.asciidoc b/docs/src/reference/the-traversal.asciidoc
index 5db65c45db..8465103f13 100644
--- a/docs/src/reference/the-traversal.asciidoc
+++ b/docs/src/reference/the-traversal.asciidoc
@@ -638,7 +638,8 @@ g.addE('knows').from(__.V(1)).to(__.constant(6)) <7>
 *Additional References*
 
 link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.html#addE(java.lang.String)++[`addE(String)`],
-link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.html#addE(org.apache.tinkerpop.gremlin.process.traversal.Traversal)++[`addE(Traversal)`]
+link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.html#addE(org.apache.tinkerpop.gremlin.process.traversal.Traversal)++[`addE(Traversal)`],
+link:https://tinkerpop.apache.org/docs/x.y.z/dev/provider/#tiny-gremlin[Tiny Gremlin Semantics]
 
 [[addvertex-step]]
 === AddV Step
@@ -659,7 +660,8 @@ g.V().has('name','nothing').bothE()
 
 link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.html#addV()++[`addV()`],
 link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.html#addV(java.lang.String)++[`addV(String)`],
-link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.html#addV(org.apache.tinkerpop.gremlin.process.traversal.Traversal)++[`addV(Traversal)`]
+link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.html#addV(org.apache.tinkerpop.gremlin.process.traversal.Traversal)++[`addV(Traversal)`],
+link:https://tinkerpop.apache.org/docs/x.y.z/dev/provider/#tiny-gremlin[Tiny Gremlin Semantics]
 
 [[aggregate-step]]
 === [[store-step]]Aggregate Step
@@ -1808,7 +1810,8 @@ g.inject(1).coalesce(E().hasLabel("knows"), addE("knows").from(V().has("name","j
 *Additional References*
 
 link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.html#E(java.lang.Object...)++[`E(Object...)`],
-link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.html#E(org.apache.tinkerpop.gremlin.process.traversal.Traversal)++[`E(Traversal)`]
+link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.html#E(org.apache.tinkerpop.gremlin.process.traversal.Traversal)++[`E(Traversal)`],
+link:https://tinkerpop.apache.org/docs/x.y.z/dev/provider/#tiny-gremlin[Tiny Gremlin Semantics]
 
 [[element-step]]
 === Element Step
@@ -1859,7 +1862,8 @@ as the `id` is the only available data to the star graph.
 
 *Additional References*
 
-link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.html#elementMap(java.lang.String...)++[`elementMap(String...)`]
+link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.html#elementMap(java.lang.String...)++[`elementMap(String...)`],
+link:https://tinkerpop.apache.org/docs/x.y.z/dev/provider/#tiny-gremlin[Tiny Gremlin Semantics]
 
 [[emit-step]]
 === Emit Step
@@ -2059,7 +2063,8 @@ The term `from` is a reserved word in Python, and therefore must be referred to
 
 link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.html#from(java.lang.String)++[`from(String)`],
 link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.html#from(org.apache.tinkerpop.gremlin.process.traversal.Traversal)++[`from(Traversal)`],
-link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.html#from(org.apache.tinkerpop.gremlin.structure.Vertex)++[`from(Vertex)`]
+link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.html#from(org.apache.tinkerpop.gremlin.structure.Vertex)++[`from(Vertex)`],
+link:https://tinkerpop.apache.org/docs/x.y.z/dev/provider/#tiny-gremlin[Tiny Gremlin Semantics]
 
 [[group-step]]
 === Group Step
@@ -2229,7 +2234,8 @@ link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gre
 link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.html#hasValue(org.apache.tinkerpop.gremlin.process.traversal.P)++[`hasValue(P)`],
 link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/P.html++[`P`],
 link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/TextP.html++[`TextP`],
-link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/structure/T.html++[`T`]
+link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/structure/T.html++[`T`],
+link:https://tinkerpop.apache.org/docs/x.y.z/dev/provider/#tiny-gremlin[Tiny Gremlin Semantics]
 
 [[id-step]]
 === Id Step
@@ -2246,7 +2252,8 @@ g.V(1).properties().id()
 
 *Additional References*
 
-link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.html#id()++[`id()`]
+link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.html#id()++[`id()`],
+link:https://tinkerpop.apache.org/docs/x.y.z/dev/provider/#tiny-gremlin[Tiny Gremlin Semantics]
 
 [[identity-step]]
 === Identity Step
@@ -2588,7 +2595,8 @@ g.V(1).properties().properties().key()
 
 *Additional References*
 
-link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.html#key()++[`key()`]
+link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.html#key()++[`key()`],
+link:https://tinkerpop.apache.org/docs/x.y.z/dev/provider/#tiny-gremlin[Tiny Gremlin Semantics]
 
 [[label-step]]
 === Label Step
@@ -2604,7 +2612,8 @@ g.V(1).properties().label()
 
 *Additional References*
 
-link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.html#label()++[`label()`]
+link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.html#label()++[`label()`],
+link:https://tinkerpop.apache.org/docs/x.y.z/dev/provider/#tiny-gremlin[Tiny Gremlin Semantics]
 
 [[length-step]]
 === Length Step
@@ -2656,8 +2665,9 @@ g.V().valueMap().select('location').limit(local, 1).unfold() <4>
 *Additional References*
 
 link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.html#limit(long)++[`limit(long)`],
-link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.html#limit(org.apache.tinkerpop.gremlin.process.traversal.Scope,long)++[`limit(Scope,long)`]
-link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/Scope.html++[`Scope`]
+link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.html#limit(org.apache.tinkerpop.gremlin.process.traversal.Scope,long)++[`limit(Scope,long)`],
+link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/Scope.html++[`Scope`],
+link:https://tinkerpop.apache.org/docs/x.y.z/dev/provider/#tiny-gremlin[Tiny Gremlin Semantics]
 
 [[local-step]]
 === Local Step
@@ -3810,7 +3820,8 @@ order. Those tokens were deprecated and eventually removed in 3.5.0.
 link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.html#order()++[`order()`],
 link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.html#order(org.apache.tinkerpop.gremlin.process.traversal.Scope)++[`order(Scope)`],
 link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/Scope.html++[`Scope`],
-link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/Order.html++[`Order`]
+link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/Order.html++[`Order`],
+link:https://tinkerpop.apache.org/docs/x.y.z/dev/provider/#tiny-gremlin[Tiny Gremlin Semantics]
 
 [[pagerank-step]]
 === PageRank Step
@@ -3951,7 +3962,8 @@ path.d == path.e
 
 *Additional References*
 
-link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.html#path()++[`path()`]
+link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.html#path()++[`path()`],
+link:https://tinkerpop.apache.org/docs/x.y.z/dev/provider/#tiny-gremlin[Tiny Gremlin Semantics]
 
 [[peerpressure-step]]
 === PeerPressure Step
@@ -4223,7 +4235,8 @@ mutating steps (`IllegalArgumentException`) and must produce a `Map` (`IllegalSt
 
 link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.html#property(java.lang.Object,java.lang.Object,java.lang.Object...)++[`property(Object, Object, Object...)`],
 link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.html#property(org.apache.tinkerpop.gremlin.structure.VertexProperty.Cardinality,java.lang.Object,java.lang.Object,java.lang.Object...)++[`property(Cardinality, Object, Object, Object...)`],
-link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/structure/VertexProperty.Cardinality.html++[`Cardinality`]
+link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/structure/VertexProperty.Cardinality.html++[`Cardinality`],
+link:https://tinkerpop.apache.org/docs/x.y.z/dev/provider/#tiny-gremlin[Tiny Gremlin Semantics]
 
 [[propertymap-step]]
 === PropertyMap Step
@@ -4286,7 +4299,8 @@ g.V().valueMap().select('location').range(local, 1, 2).unfold() <3>
 
 link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.html#range(long,long)++[`range(long,long)`],
 link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.html#range(org.apache.tinkerpop.gremlin.process.traversal.Scope,long,long)++[`range(Scope,long,long)`],
-link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/Scope.html++[`Scope`]
+link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/Scope.html++[`Scope`],
+link:https://tinkerpop.apache.org/docs/x.y.z/dev/provider/#tiny-gremlin[Tiny Gremlin Semantics]
 
 [[read-step]]
 === Read Step
@@ -4922,7 +4936,8 @@ g.V().hasLabel('person').filter(outE('created')).as('p'). <1>
 
 link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.html#skip(long)++[`skip(long)`],
 link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.html#skip(org.apache.tinkerpop.gremlin.process.traversal.Scope,long)++[`skip(Scope,long)`],
-link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/Scope.html++[`Scope`]
+link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/Scope.html++[`Scope`],
+link:https://tinkerpop.apache.org/docs/x.y.z/dev/provider/#tiny-gremlin[Tiny Gremlin Semantics]
 
 [[split-step]]
 === Split Step
@@ -5141,9 +5156,10 @@ g.V().elementMap().tail(local) <6>
 
 link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.html#tail()++[`tail()`],
 link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.html#tail(long)++[`tail(long)`],
-link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.html#tail(org.apache.tinkerpop.gremlin.process.traversal.Scope)++[`tail(Scope)`]
-link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.html#tail(org.apache.tinkerpop.gremlin.process.traversal.Scope,long)++[`tail(Scope,long)`]
-link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/Scope.html++[`Scope`]
+link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.html#tail(org.apache.tinkerpop.gremlin.process.traversal.Scope)++[`tail(Scope)`],
+link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.html#tail(org.apache.tinkerpop.gremlin.process.traversal.Scope,long)++[`tail(Scope,long)`],
+link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/Scope.html++[`Scope`],
+link:https://tinkerpop.apache.org/docs/x.y.z/dev/provider/#tiny-gremlin[Tiny Gremlin Semantics]
 
 [[timelimit-step]]
 === TimeLimit Step
@@ -5205,7 +5221,8 @@ link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gre
 link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.html#to(org.apache.tinkerpop.gremlin.structure.Vertex)++[`to(Vertex)`],
 link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.html#toE(org.apache.tinkerpop.gremlin.structure.Direction,java.lang.String...)++[`toE(Direction,String)`],
 link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.html#toV(org.apache.tinkerpop.gremlin.structure.Direction)++[`toV(Direction)`],
-link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/structure/Direction.html++[`Direction`]
+link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/structure/Direction.html++[`Direction`],
+link:https://tinkerpop.apache.org/docs/x.y.z/dev/provider/#tiny-gremlin[Tiny Gremlin Semantics]
 
 [[toLower-step]]
 === ToLower Step
@@ -5430,7 +5447,8 @@ g.V().has('name', within('marko', 'vadas', 'josh')).as('person').
 *Additional References*
 
 link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.html#V(java.lang.Object...)++[`V(Object...)`],
-link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.html#V(org.apache.tinkerpop.gremlin.process.traversal.Traversal)++[`V(Traversal)`]
+link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.html#V(org.apache.tinkerpop.gremlin.process.traversal.Traversal)++[`V(Traversal)`],
+link:https://tinkerpop.apache.org/docs/x.y.z/dev/provider/#tiny-gremlin[Tiny Gremlin Semantics]
 
 [[value-step]]
 === Value Step
@@ -5445,7 +5463,8 @@ g.V(1).properties().properties().value()
 
 *Additional References*
 
-link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.html#value()++[`value()`]
+link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.html#value()++[`value()`],
+link:https://tinkerpop.apache.org/docs/x.y.z/dev/provider/#tiny-gremlin[Tiny Gremlin Semantics]
 
 [[valuemap-step]]
 === ValueMap Step
@@ -5496,7 +5515,8 @@ g.V().hasLabel('person').properties('location').valueMap().with(WithOptions.toke
 
 *Additional References*
 
-link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.html#valueMap(java.lang.String...)++[`valueMap(String...)`]
+link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.html#valueMap(java.lang.String...)++[`valueMap(String...)`],
+link:https://tinkerpop.apache.org/docs/x.y.z/dev/provider/#tiny-gremlin[Tiny Gremlin Semantics]
 
 [[values-step]]
 === Values Step
@@ -5512,7 +5532,8 @@ g.V(1).properties('location').values()
 
 *Additional References*
 
-link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.html#values(java.lang.String...)++[`values(String...)`]
+link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.html#values(java.lang.String...)++[`values(String...)`],
+link:https://tinkerpop.apache.org/docs/x.y.z/dev/provider/#tiny-gremlin[Tiny Gremlin Semantics]
 
 [[vertex-steps]]
 === Vertex Steps
@@ -5581,7 +5602,8 @@ link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gre
 link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.html#otherV()++[`otherV()`],
 link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.html#out(java.lang.String...)++[`out(String...)`],
 link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.html#outE(java.lang.String...)++[`outE(String...)`],
-link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.html#outV()++[`outV()`]
+link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.html#outV()++[`outV()`],
+link:https://tinkerpop.apache.org/docs/x.y.z/dev/provider/#tiny-gremlin[Tiny Gremlin Semantics]
 
 [[where-step]]
 === Where Step
@@ -6839,3 +6861,65 @@ t = g.V().has_label('person')
 script = t.gremlin_lang.get_gremlin()
 # Returns: "g.V().hasLabel('person')"
 ----
+
+[[tiny-gremlin-traversal]]
+== Tiny Gremlin
+
+*Tiny Gremlin* is a defined subset of the Gremlin traversal language that executes locally and in-process against a
+small, detached `Graph` instance. The most common use case is to call `subgraph()` on a remote graph, receive a `Graph`
+object, and then navigate and filter it locally using familiar Gremlin syntax without an additional network round-trip.
+A secondary use case is constructing small graphs entirely in-process using `addV()`, `addE()`, and `property()`.
+
+Full details on semantics, supported forms, and limitations are covered in the
+link:https://tinkerpop.apache.org/docs/x.y.z/dev/provider/#tiny-gremlin[Tiny Gremlin Semantics] section of the
+Provider Documentation. Tiny Gremlin is currently only supported in `gremlin-javascript`.
+
+=== Supported Steps
+
+Tiny Gremlin supports exactly 33 steps. No other step may be used in a Tiny Gremlin traversal; attempting to do so
+results in an error before any graph data is accessed.
+
+[width="100%",options="header"]
+|===
+|Category |Steps
+|Source |<<v-step,`V()`>>, <<e-step,`E()`>>
+|Navigation |<<vertex-steps,`out()`>>, <<vertex-steps,`in()`>>, <<vertex-steps,`both()`>>, <<vertex-steps,`outE()`>>, <<vertex-steps,`inE()`>>, <<vertex-steps,`bothE()`>>, <<vertex-steps,`outV()`>>, <<vertex-steps,`inV()`>>, <<vertex-steps,`otherV()`>>
+|Filter |<<has-step,`has()`>>, <<has-step,`hasId()`>>, <<has-step,`hasLabel()`>>, <<has-step,`hasNot()`>>
+|Value Extraction |<<id-step,`id()`>>, <<label-step,`label()`>>, <<values-step,`values()`>>, <<valuemap-step,`valueMap()`>>, <<elementmap-step,`elementMap()`>>, <<value-step,`value()`>>, <<key-step,`key()`>>
+|Path |<<path-step,`path()`>>
+|Range |<<limit-step,`limit()`>>, <<range-step,`range()`>>, <<skip-step,`skip()`>>, <<tail-step,`tail()`>>
+|Ordering |<<order-step,`order()`>>
+|Mutation |<<addvertex-step,`addV()`>>, <<addedge-step,`addE()`>>, <<property-step,`property()`>>
+|Modulators |<<from-step,`from()`>>, <<to-step,`to()`>>
+|===
+
+=== Language Support
+
+Tiny Gremlin is currently only implemented in `gremlin-javascript`. The `with_(graph)` API accepts a local `Graph`
+object in place of a remote connection, transparently enabling local traversal using the same Gremlin API:
+
+[source,javascript]
+----
+const { graph: { Graph }, process: { anonymousTraversal: { traversal } } } = require('gremlin');
+
+const graph = new Graph();
+const g = traversal().with_(graph);
+
+// Build a small graph and query it locally
+const names = await g.addV('person').property('name', 'alice')
+                      .V().values('name').toList();
+// Returns: ['alice']
+----
+
+The primary use case of processing a subgraph received from a remote server requires no code changes other than
+passing the returned `Graph` object to `with_()`:
+
+[source,javascript]
+----
+// Retrieve a subgraph from a remote server
+const subgraph = await gRemote.V().hasLabel('person').subgraph('sg').cap('sg').next();
+
+// Query it locally with Tiny Gremlin — no additional network calls
+const g = traversal().with_(subgraph);
+const paths = await g.V().out('knows').in('knows').path().toList();
+----
diff --git a/docs/static/images/tiny-gremlin.png b/docs/static/images/tiny-gremlin.png
new file mode 100644
index 0000000000..67a53f8984
Binary files /dev/null and b/docs/static/images/tiny-gremlin.png differ
diff --git a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs
index e6238de9a6..0d610f47d9 100644
--- a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs
+++ b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs
@@ -563,6 +563,7 @@ namespace Gremlin.Net.IntegrationTest.Gherkin
                {"g_E_hasXlabel_nullX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.E().Has(T.Label, (object) null)}}, 
                {"g_V_properties_hasLabelXnullX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.V().Properties<object>().HasLabel((string) null)}}, 
                {"g_V_hasNotXageX_name", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.V().HasNot("age").Values<object>("name")}}, 
+               {"g_V_hasNotXlangX_name", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.V().HasNot("lang").Values<object>("name")}}, 
                {"g_V_properties_hasValueXnullX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.V().Properties<object>().HasValue((object) null)}}, 
                {"g_V_properties_hasValueXnull_nullX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.V().Properties<object>().HasValue(null, null)}}, 
                {"g_V_properties_hasValueXnull_joshX_value", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.V().Properties<object>().HasValue(null, "josh").Value<object>()}}, 
@@ -1645,6 +1646,9 @@ namespace Gremlin.Net.IntegrationTest.Gherkin
                {"g_VX1X_elementMap_orderXlocalX_byXkeys_descXunfold", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.V(p["vid1"]).ElementMap<object>().Order(Scope.Local).By(Column.Keys, Order.Desc).Unfold<object>()}}, 
                {"g_VX1X_elementMap_orderXlocalX_byXkeys_ascXunfold", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.V(p["vid1"]).ElementMap<object>().Order(Scope.Local).By(Column.Keys, Order.Asc).Unfold<object>()}}, 
                {"g_VX1X_valuesXageX_orderXlocalX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.V(p["vid1"]).Values<object>("age").Order(Scope.Local)}}, 
+               {"g_V_order_byXTidX_name", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.V().Order().By(T.Id).Values<object>("name")}}, 
+               {"g_V_order_byXTid_descX_name", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.V().Order().By(T.Id, Order.Desc).Values<object>("name")}}, 
+               {"g_V_order_byXTlabelX_label", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.V().Order().By(T.Label).Label()}}, 
                {"g_V_pageRank_hasXpageRankX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.V().PageRank().Has("gremlin.pageRankVertexProgram.pageRank")}}, 
                {"g_V_outXcreatedX_pageRank_withXedges_bothEX_withXpropertyName_projectRankX_withXtimes_0X_valueMapXname_projectRankX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.V().Out("created").PageRank().With("~tinkerpop.pageRank.edges", __.BothE()).With("~tinkerpop.pageRank.propertyName", "projectRank").With("~tinkerpop.pageRank.times", 0).ValueMap<object, object>("name", "projectRank")}}, 
                {"g_V_pageRank_order_byXpageRank_descX_byXnameX_name", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.V().PageRank().Order().By("gremlin.pageRankVertexProgram.pageRank", Order.Desc).By("name").Values<object>("name")}}, 
@@ -1665,6 +1669,7 @@ namespace Gremlin.Net.IntegrationTest.Gherkin
                {"g_withStrategiesXProductiveByStrategyX_VX1X_out_path_byXageX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.WithStrategies(new ProductiveByStrategy()).V(p["vid1"]).Out().Path().By("age")}}, 
                {"g_injectX1_null_nullX_path", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.Inject<object>(1, null, null).Path()}}, 
                {"g_injectX1_null_nullX_path_dedup", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.Inject<object>(1, null, null).Path().Dedup()}}, 
+               {"g_VX1X_out_out_path", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.V(p["vid1"]).Out().Out().Path()}}, 
                {"g_V_peerPressure_hasXclusterX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.V().PeerPressure().Has("gremlin.peerPressureVertexProgram.cluster")}}, 
                {"g_V_peerPressure_withXpropertyName_clusterX_withXedges_outEXknowsXX_pageRankX1X_byXrankX_withXedges_outEXknowsX_withXtimes_2X_group_byXclusterX_byXrank_sumX_limitX100X", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.V().PeerPressure().With("~tinkerpop.peerPressure.propertyName", "cluster").With("~tinkerpop.peerPressure.edges", __.OutE("knows")).PageRank(1.0).With("~tinkerpop.pageRank.propertyName", "rank").With("~tinkerpop.pageR [...]
                {"g_V_hasXname_rippleX_inXcreatedX_peerPressure_withXedges_outEX_withyXpropertyName_clusterX_repeatXunionXidentity__bothX_timesX2X_dedup_valueMapXname_clusterX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.V().Has("name", "ripple").In("created").PeerPressure().With("~tinkerpop.peerPressure.edges", __.OutE()).With("~tinkerpop.peerPressure.propertyName", "cluster").Repeat(__.Union<object>(__.Identity(), __.Both())).Times(2).Dedup( [...]
diff --git a/gremlin-go/driver/cucumber/gremlin.go b/gremlin-go/driver/cucumber/gremlin.go
index 82f5ff777b..70822b8eba 100644
--- a/gremlin-go/driver/cucumber/gremlin.go
+++ b/gremlin-go/driver/cucumber/gremlin.go
@@ -533,6 +533,7 @@ var translationMap = map[string][]func(g *gremlingo.GraphTraversalSource, p map[
     "g_E_hasXlabel_nullX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.E().Has(gremlingo.T.Label, nil)}}, 
     "g_V_properties_hasLabelXnullX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Properties().HasLabel(nil)}}, 
     "g_V_hasNotXageX_name": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().HasNot("age").Values("name")}}, 
+    "g_V_hasNotXlangX_name": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().HasNot("lang").Values("name")}}, 
     "g_V_properties_hasValueXnullX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Properties().HasValue(nil)}}, 
     "g_V_properties_hasValueXnull_nullX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Properties().HasValue(nil, nil)}}, 
     "g_V_properties_hasValueXnull_joshX_value": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Properties().HasValue(nil, "josh").Value()}}, 
@@ -1615,6 +1616,9 @@ var translationMap = map[string][]func(g *gremlingo.GraphTraversalSource, p map[
     "g_VX1X_elementMap_orderXlocalX_byXkeys_descXunfold": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V(p["vid1"]).ElementMap().Order(gremlingo.Scope.Local).By(gremlingo.Column.Keys, gremlingo.Order.Desc).Unfold()}}, 
     "g_VX1X_elementMap_orderXlocalX_byXkeys_ascXunfold": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V(p["vid1"]).ElementMap().Order(gremlingo.Scope.Local).By(gremlingo.Column.Keys, gremlingo.Order.Asc).Unfold()}}, 
     "g_VX1X_valuesXageX_orderXlocalX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V(p["vid1"]).Values("age").Order(gremlingo.Scope.Local)}}, 
+    "g_V_order_byXTidX_name": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Order().By(gremlingo.T.Id).Values("name")}}, 
+    "g_V_order_byXTid_descX_name": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Order().By(gremlingo.T.Id, gremlingo.Order.Desc).Values("name")}}, 
+    "g_V_order_byXTlabelX_label": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Order().By(gremlingo.T.Label).Label()}}, 
     "g_V_pageRank_hasXpageRankX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().PageRank().Has("gremlin.pageRankVertexProgram.pageRank")}}, 
     "g_V_outXcreatedX_pageRank_withXedges_bothEX_withXpropertyName_projectRankX_withXtimes_0X_valueMapXname_projectRankX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Out("created").PageRank().With("~tinkerpop.pageRank.edges", gremlingo.T__.BothE()).With("~tinkerpop.pageRank.propertyName", "projectRank").With("~tinkerpop.pageRank.times", 0).ValueMap("name", "projectRank")}}, 
     "g_V_pageRank_order_byXpageRank_descX_byXnameX_name": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().PageRank().Order().By("gremlin.pageRankVertexProgram.pageRank", gremlingo.Order.Desc).By("name").Values("name")}}, 
@@ -1635,6 +1639,7 @@ var translationMap = map[string][]func(g *gremlingo.GraphTraversalSource, p map[
     "g_withStrategiesXProductiveByStrategyX_VX1X_out_path_byXageX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.WithStrategies(gremlingo.ProductiveByStrategy()).V(p["vid1"]).Out().Path().By("age")}}, 
     "g_injectX1_null_nullX_path": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Inject(1, nil, nil).Path()}}, 
     "g_injectX1_null_nullX_path_dedup": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Inject(1, nil, nil).Path().Dedup()}}, 
+    "g_VX1X_out_out_path": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V(p["vid1"]).Out().Out().Path()}}, 
     "g_V_peerPressure_hasXclusterX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().PeerPressure().Has("gremlin.peerPressureVertexProgram.cluster")}}, 
     "g_V_peerPressure_withXpropertyName_clusterX_withXedges_outEXknowsXX_pageRankX1X_byXrankX_withXedges_outEXknowsX_withXtimes_2X_group_byXclusterX_byXrank_sumX_limitX100X": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().PeerPressure().With("~tinkerpop.peerPressure.propertyName", "cluster").With("~tinkerpop.peerPressure.edges", gremlingo.T__.OutE("knows")).PageRank(1.0).With("~tinkerpop.pageRank.propertyName", "rank").With("~ti [...]
     "g_V_hasXname_rippleX_inXcreatedX_peerPressure_withXedges_outEX_withyXpropertyName_clusterX_repeatXunionXidentity__bothX_timesX2X_dedup_valueMapXname_clusterX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Has("name", "ripple").In("created").PeerPressure().With("~tinkerpop.peerPressure.edges", gremlingo.T__.OutE()).With("~tinkerpop.peerPressure.propertyName", "cluster").Repeat(gremlingo.T__.Union(gremlingo.T__.Identity(),  [...]
diff --git a/gremlin-js/gremlin-javascript/lib/driver/local-graph-connection.ts b/gremlin-js/gremlin-javascript/lib/driver/local-graph-connection.ts
new file mode 100644
index 0000000000..0828caf8f9
--- /dev/null
+++ b/gremlin-js/gremlin-javascript/lib/driver/local-graph-connection.ts
@@ -0,0 +1,120 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import {
+  ATNSimulator, BaseErrorListener, CharStream, CommonTokenStream,
+  PredictionMode, Recognizer, RecognitionException,
+} from 'antlr4ng';
+import { GremlinLexer } from '../language/grammar/GremlinLexer.js';
+import { GremlinParser } from '../language/grammar/GremlinParser.js';
+import { ExecuteVisitor } from '../language/executor/ExecuteVisitor.js';
+import { LocalExecutor } from '../process/local/LocalExecutor.js';
+import { RemoteConnection, RemoteTraversal } from './remote-connection.js';
+import { Graph } from '../structure/graph.js';
+import type GremlinLang from '../process/gremlin-lang.js';
+
+class ThrowingErrorListener extends BaseErrorListener {
+  override syntaxError(
+    _recognizer: Recognizer<ATNSimulator>,
+    _offendingSymbol: unknown,
+    line: number,
+    column: number,
+    msg: string,
+    _e: RecognitionException | null,
+  ): void {
+    throw new Error(`Tiny Gremlin parse error at ${line}:${column} — ${msg}`);
+  }
+}
+
+const errorListener = new ThrowingErrorListener();
+
+/**
+ * A RemoteConnection implementation that executes traversals locally against an
+ * in-memory Graph using the Tiny Gremlin subset. No network is involved.
+ *
+ * Usage:
+ *   const g = anon.traversal().with_(myGraph);
+ *   // or equivalently:
+ *   const g = anon.traversal().with_(new LocalGraphConnection(myGraph));
+ */
+export class LocalGraphConnection extends RemoteConnection {
+  private readonly graph: Graph;
+  private readonly executor: LocalExecutor;
+
+  constructor(graph: Graph) {
+    super('local://');
+    this.graph = graph;
+    this.executor = new LocalExecutor();
+  }
+
+  override async open(): Promise<void> {
+    // no-op: no connection to open
+  }
+
+  override get isOpen(): boolean {
+    return true;
+  }
+
+  override async submit(gremlinLang: GremlinLang): Promise<RemoteTraversal> {
+    const query = gremlinLang.getGremlin();
+    const pipeline = this.parse(query);
+    const generator = this.executor.execute(pipeline, this.graph);
+    return new RemoteTraversal(generator);
+  }
+
+  override async commit(): Promise<void> {
+    // no-op: local graph mutations are in-memory only
+  }
+
+  override async rollback(): Promise<void> {
+    // no-op
+  }
+
+  override async close(): Promise<void> {
+    // no-op
+  }
+
+  private parse(query: string) {
+    const chars = CharStream.fromString(query);
+    const lexer = new GremlinLexer(chars);
+    lexer.removeErrorListeners();
+    lexer.addErrorListener(errorListener);
+
+    const tokens = new CommonTokenStream(lexer);
+    const parser = new GremlinParser(tokens);
+    parser.interpreter.predictionMode = PredictionMode.SLL;
+    parser.removeErrorListeners();
+    parser.addErrorListener(errorListener);
+
+    let queryCtx: ReturnType<typeof parser.queryList>;
+    try {
+      queryCtx = parser.queryList();
+    } catch {
+      tokens.seek(0);
+      lexer.reset();
+      parser.reset();
+      parser.interpreter.predictionMode = PredictionMode.LL;
+      queryCtx = parser.queryList();
+    }
+
+    const visitor = new ExecuteVisitor();
+    visitor.visit(queryCtx);
+    return visitor.getPipeline();
+  }
+}
diff --git a/gremlin-js/gremlin-javascript/lib/index.ts b/gremlin-js/gremlin-javascript/lib/index.ts
index a2443b8d1d..3498fa59a3 100644
--- a/gremlin-js/gremlin-javascript/lib/index.ts
+++ b/gremlin-js/gremlin-javascript/lib/index.ts
@@ -36,6 +36,8 @@ import ResultSet from './driver/result-set.js';
 import { HttpRequest } from './driver/http-request.js';
 import { basic, sigv4 } from './driver/auth.js';
 import AnonymousTraversalSource from './process/anonymous-traversal.js';
+import { LocalGraphConnection } from './driver/local-graph-connection.js';
+import { LocalExecutionError } from './process/local/LocalExecutionError.js';
 
 export const driver = {
   RemoteConnection: rc.RemoteConnection,
@@ -43,6 +45,7 @@ export const driver = {
   RemoteTraversal: rc.RemoteTraversal,
   ResponseError,
   DriverRemoteConnection,
+  LocalGraphConnection,
   Client,
   ResultSet,
   HttpRequest,
@@ -50,6 +53,7 @@ export const driver = {
     basic,
     sigv4,
   },
+  LocalExecutionError,
 };
 
 export const process = {
diff --git a/gremlin-js/gremlin-javascript/lib/language/executor/ExecuteVisitor.ts b/gremlin-js/gremlin-javascript/lib/language/executor/ExecuteVisitor.ts
new file mode 100644
index 0000000000..99957284f2
--- /dev/null
+++ b/gremlin-js/gremlin-javascript/lib/language/executor/ExecuteVisitor.ts
@@ -0,0 +1,708 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { P, TextP } from '../../process/traversal.js';
+import type { Arg, Pipeline, StepDescriptor } from '../../process/local/types.js';
+
+/**
+ * Converts an ANTLR parse tree produced by GremlinParser into a StepDescriptor[]
+ * (Pipeline) suitable for execution by LocalExecutor.
+ *
+ * Uses the same className→visitXxx dispatch mechanism as TranslateVisitor but
+ * accumulates typed step descriptors rather than output strings.
+ */
+export class ExecuteVisitor {
+  private readonly steps: StepDescriptor[] = [];
+
+  getPipeline(): Pipeline {
+    return this.steps;
+  }
+
+  // ── Dispatch ──────────────────────────────────────────────────────────────
+
+  visit(tree: any): void {
+    if (tree == null) return;
+    const className: string = tree?.constructor?.name ?? '';
+    if (className.endsWith('Context')) {
+      const methodName = 'visit' + className.slice(0, -'Context'.length);
+      const method = (this as any)[methodName];
+      if (typeof method === 'function') {
+        method.call(this, tree);
+        return;
+      }
+    }
+    // Fall back to visiting children for unhandled intermediate nodes
+    this.visitChildren(tree);
+  }
+
+  private visitChildren(node: any): void {
+    const n = node.getChildCount?.() ?? 0;
+    for (let i = 0; i < n; i++) {
+      this.visit(node.getChild(i));
+    }
+  }
+
+  // ── Top-level structure ───────────────────────────────────────────────────
+
+  visitQueryList(ctx: any): void {
+    // queryList → query → rootTraversal | terminatedTraversal
+    const n = ctx.getChildCount();
+    for (let i = 0; i < n; i++) {
+      const child = ctx.getChild(i);
+      const name = child?.constructor?.name ?? '';
+      if (name === 'QueryContext') {
+        this.visit(child);
+        return;
+      }
+    }
+    this.visitChildren(ctx);
+  }
+
+  visitQuery(ctx: any): void {
+    this.visitChildren(ctx);
+  }
+
+  visitRootTraversal(ctx: any): void {
+    // traversalSource DOT traversalSourceSpawnMethod (DOT chainedTraversal)?
+    this.visit(ctx.traversalSourceSpawnMethod());
+    const chained = ctx.chainedTraversal?.();
+    if (chained) this.visitChainedTraversal(chained);
+  }
+
+  visitTerminatedTraversal(ctx: any): void {
+    this.visit(ctx.rootTraversal?.() ?? ctx.getChild(0));
+  }
+
+  visitChainedTraversal(ctx: any): void {
+    // Grammar is left-recursive: outermost node holds the LAST step, innermost holds the FIRST.
+    // Recurse into the left subtree first, then append the current (rightmost) step.
+    const next = ctx.chainedTraversal?.();
+    if (next) this.visitChainedTraversal(next);
+    this.visit(ctx.traversalMethod());
+  }
+
+  visitTraversalSourceSpawnMethod(ctx: any): void {
+    this.visitChildren(ctx);
+  }
+
+  visitTraversalMethod(ctx: any): void {
+    // Find the specific TraversalMethod_* child and dispatch to it.
+    // If no specific visitor exists, push a placeholder so validation can reject it.
+    const n = ctx.getChildCount?.() ?? 0;
+    for (let i = 0; i < n; i++) {
+      const child = ctx.getChild(i);
+      const name: string = child?.constructor?.name ?? '';
+      if (name.startsWith('TraversalMethod_') && name.endsWith('Context')) {
+        const visitorName = 'visit' + name.slice(0, -'Context'.length);
+        if (typeof (this as any)[visitorName] === 'function') {
+          (this as any)[visitorName](child);
+        } else {
+          // Unknown step — extract name (e.g. 'TraversalMethod_repeatContext' → 'repeat')
+          const raw = name.slice('TraversalMethod_'.length, -'Context'.length).split('_')[0];
+          this.push(raw, []);
+        }
+        return;
+      }
+    }
+  }
+
+  // ── Nested traversal → sub-pipeline ──────────────────────────────────────
+
+  private visitNestedTraversalAsSubPipeline(ctx: any): Pipeline {
+    const sub = new ExecuteVisitor();
+    // nestedTraversal has a chainedTraversal child
+    const chain = ctx.chainedTraversal?.();
+    if (chain) sub.visitChainedTraversal(chain);
+    return sub.getPipeline();
+  }
+
+  // ── Mid-traversal V/E (used inside anonymous traversals, e.g. __.V(1)) ──────
+
+  visitTraversalMethod_V(ctx: any): void {
+    const args = this.recoverGenericVarargs(ctx.genericArgumentVarargs());
+    this.push('V', args);
+  }
+
+  visitTraversalMethod_E(ctx: any): void {
+    const args = this.recoverGenericVarargs(ctx.genericArgumentVarargs());
+    this.push('E', args);
+  }
+
+  // ── Source spawn methods (g.V(), g.E(), g.addV(), g.addE()) ──────────────
+
+  visitTraversalSourceSpawnMethod_V(ctx: any): void {
+    const args = this.recoverGenericVarargs(ctx.genericArgumentVarargs());
+    this.push('V', args);
+  }
+
+  visitTraversalSourceSpawnMethod_E(ctx: any): void {
+    const args = this.recoverGenericVarargs(ctx.genericArgumentVarargs());
+    this.push('E', args);
+  }
+
+  visitTraversalSourceSpawnMethod_addV(ctx: any): void {
+    const strArg = ctx.stringArgument?.();
+    if (strArg) {
+      this.push('addV', [this.recoverStringArg(strArg)]);
+    } else {
+      this.push('addV', []);
+    }
+  }
+
+  visitTraversalSourceSpawnMethod_addE(ctx: any): void {
+    const strArg = ctx.stringArgument?.();
+    if (strArg) {
+      this.push('addE', [this.recoverStringArg(strArg)]);
+    } else {
+      // Traversal label form — not supported in Tiny Gremlin
+      this.push('addE(Traversal)', []);
+    }
+  }
+
+  // ── Navigation steps ──────────────────────────────────────────────────────
+
+  visitTraversalMethod_out(ctx: any): void {
+    this.push('out', this.recoverStringNullableVarargs(ctx.stringNullableArgumentVarargs()));
+  }
+
+  visitTraversalMethod_in(ctx: any): void {
+    this.push('in', this.recoverStringNullableVarargs(ctx.stringNullableArgumentVarargs()));
+  }
+
+  visitTraversalMethod_both(ctx: any): void {
+    this.push('both', this.recoverStringNullableVarargs(ctx.stringNullableArgumentVarargs()));
+  }
+
+  visitTraversalMethod_outE(ctx: any): void {
+    this.push('outE', this.recoverStringNullableVarargs(ctx.stringNullableArgumentVarargs()));
+  }
+
+  visitTraversalMethod_inE(ctx: any): void {
+    this.push('inE', this.recoverStringNullableVarargs(ctx.stringNullableArgumentVarargs()));
+  }
+
+  visitTraversalMethod_bothE(ctx: any): void {
+    this.push('bothE', this.recoverStringNullableVarargs(ctx.stringNullableArgumentVarargs()));
+  }
+
+  visitTraversalMethod_outV(ctx: any): void {
+    this.push('outV', []);
+  }
+
+  visitTraversalMethod_inV(ctx: any): void {
+    this.push('inV', []);
+  }
+
+  visitTraversalMethod_otherV(ctx: any): void {
+    this.push('otherV', []);
+  }
+
+  // ── Filter steps ──────────────────────────────────────────────────────────
+
+  // has(key) — existence check
+  visitTraversalMethod_has_String(ctx: any): void {
+    this.push('has', [this.recoverStringNullableLiteral(ctx.stringNullableLiteral())]);
+  }
+
+  // has(key, value)
+  visitTraversalMethod_has_String_Object(ctx: any): void {
+    this.push('has', [
+      this.recoverStringNullableLiteral(ctx.stringNullableLiteral()),
+      this.recoverGenericArg(ctx.genericArgument()),
+    ]);
+  }
+
+  // has(key, P)
+  visitTraversalMethod_has_String_P(ctx: any): void {
+    this.push('has', [
+      this.recoverStringNullableLiteral(ctx.stringNullableLiteral()),
+      this.recoverPredicate(ctx.traversalPredicate()),
+    ]);
+  }
+
+  // has(label, key, value) — label is stringNullableArgument, key is stringNullableLiteral
+  visitTraversalMethod_has_String_String_Object(ctx: any): void {
+    this.push('has', [
+      this.recoverStringNullableArgument(ctx.stringNullableArgument()),
+      this.recoverStringNullableLiteral(ctx.stringNullableLiteral()),
+      this.recoverGenericArg(ctx.genericArgument()),
+    ]);
+  }
+
+  // has(label, key, P) — label is stringNullableArgument, key is stringNullableLiteral
+  visitTraversalMethod_has_String_String_P(ctx: any): void {
+    this.push('has', [
+      this.recoverStringNullableArgument(ctx.stringNullableArgument()),
+      this.recoverStringNullableLiteral(ctx.stringNullableLiteral()),
+      this.recoverPredicate(ctx.traversalPredicate()),
+    ]);
+  }
+
+  // has(T, value)
+  visitTraversalMethod_has_T_Object(ctx: any): void {
+    this.push('has', [
+      ctx.traversalT().getText(),
+      this.recoverGenericArg(ctx.genericArgument()),
+    ]);
+  }
+
+  // has(T, P)
+  visitTraversalMethod_has_T_P(ctx: any): void {
+    this.push('has', [
+      ctx.traversalT().getText(),
+      this.recoverPredicate(ctx.traversalPredicate()),
+    ]);
+  }
+
+  visitTraversalMethod_hasId_Object_Object(ctx: any): void {
+    const first = this.recoverGenericArg(ctx.genericArgument());
+    const rest = this.recoverGenericVarargs(ctx.genericArgumentVarargs?.());
+    this.push('hasId', [first, ...rest]);
+  }
+
+  visitTraversalMethod_hasId_P(ctx: any): void {
+    this.push('hasId', [this.recoverPredicate(ctx.traversalPredicate())]);
+  }
+
+  visitTraversalMethod_hasLabel_String_String(ctx: any): void {
+    const first = this.recoverStringNullableArgument(ctx.stringNullableArgument());
+    const rest = this.recoverStringNullableVarargs(ctx.stringNullableArgumentVarargs?.());
+    this.push('hasLabel', [first, ...rest]);
+  }
+
+  visitTraversalMethod_hasLabel_P(ctx: any): void {
+    this.push('hasLabel', [this.recoverPredicate(ctx.traversalPredicate())]);
+  }
+
+  visitTraversalMethod_hasNot(ctx: any): void {
+    this.push('hasNot', [this.recoverStringNullableLiteral(ctx.stringNullableLiteral())]);
+  }
+
+  visitTraversalMethod_hasKey_String_String(ctx: any): void {
+    const first = this.recoverStringNullableArgument(ctx.stringNullableArgument());
+    const rest = this.recoverStringNullableVarargs(ctx.stringNullableArgumentVarargs?.());
+    this.push('hasKey', [first, ...rest]);
+  }
+
+  visitTraversalMethod_hasKey_P(ctx: any): void {
+    this.push('hasKey', [this.recoverPredicate(ctx.traversalPredicate())]);
+  }
+
+  visitTraversalMethod_hasValue_Object_Object(ctx: any): void {
+    const first = this.recoverGenericArg(ctx.genericArgument());
+    const rest = this.recoverGenericVarargs(ctx.genericArgumentVarargs?.());
+    this.push('hasValue', [first, ...rest]);
+  }
+
+  visitTraversalMethod_hasValue_P(ctx: any): void {
+    this.push('hasValue', [this.recoverPredicate(ctx.traversalPredicate())]);
+  }
+
+  // ── Value extraction steps ────────────────────────────────────────────────
+
+  visitTraversalMethod_id(ctx: any): void {
+    this.push('id', []);
+  }
+
+  visitTraversalMethod_label(ctx: any): void {
+    this.push('label', []);
+  }
+
+  visitTraversalMethod_key(ctx: any): void {
+    this.push('key', []);
+  }
+
+  visitTraversalMethod_value(ctx: any): void {
+    this.push('value', []);
+  }
+
+  visitTraversalMethod_values(ctx: any): void {
+    this.push('values', this.recoverStringNullableLiteralVarargs(ctx.stringNullableLiteralVarargs()));
+  }
+
+  visitTraversalMethod_valueMap(ctx: any): void {
+    // valueMap() or valueMap(key, key...)
+    const varargs = ctx.stringNullableArgumentVarargs?.();
+    this.push('valueMap', varargs ? this.recoverStringNullableVarargs(varargs) : []);
+  }
+
+  visitTraversalMethod_valueMap_String(ctx: any): void {
+    this.push('valueMap', this.recoverStringNullableLiteralVarargs(ctx.stringNullableLiteralVarargs()));
+  }
+
+  visitTraversalMethod_valueMap_boolean_String(ctx: any): void {
+    const includeTokens = ctx.booleanLiteral().getText() === 'true';
+    const keys = this.recoverStringNullableLiteralVarargs(ctx.stringNullableLiteralVarargs?.());
+    // True sentinel at position 0 signals token inclusion to stepValueMap
+    this.push('valueMap', includeTokens ? [true, ...keys] : keys);
+  }
+
+  visitTraversalMethod_elementMap(ctx: any): void {
+    this.push('elementMap', this.recoverStringNullableLiteralVarargs(ctx.stringNullableLiteralVarargs()));
+  }
+
+  // ── Path step ─────────────────────────────────────────────────────────────
+
+  visitTraversalMethod_path(ctx: any): void {
+    this.push('path', []);
+  }
+
+  // ── Range steps ───────────────────────────────────────────────────────────
+
+  visitTraversalMethod_limit_long(ctx: any): void {
+    this.push('limit', [this.recoverIntegerArg(ctx.integerArgument())]);
+  }
+
+  visitTraversalMethod_limit_Scope_long(_ctx: any): void {
+    this.push('limit(Scope)', []);
+  }
+
+  visitTraversalMethod_range_long_long(ctx: any): void {
+    const args = ctx.integerArgument();
+    this.push('range', [this.recoverIntegerArg(args[0]), this.recoverIntegerArg(args[1])]);
+  }
+
+  visitTraversalMethod_range_Scope_long_long(_ctx: any): void {
+    this.push('range(Scope)', []);
+  }
+
+  visitTraversalMethod_skip_long(ctx: any): void {
+    this.push('skip', [this.recoverIntegerArg(ctx.integerArgument())]);
+  }
+
+  visitTraversalMethod_skip_Scope_long(_ctx: any): void {
+    this.push('skip(Scope)', []);
+  }
+
+  visitTraversalMethod_tail_Empty(ctx: any): void {
+    this.push('tail', [1]);
+  }
+
+  visitTraversalMethod_tail_long(ctx: any): void {
+    this.push('tail', [this.recoverIntegerArg(ctx.integerArgument())]);
+  }
+
+  visitTraversalMethod_tail_Scope(_ctx: any): void {
+    this.push('tail(Scope)', []);
+  }
+
+  visitTraversalMethod_tail_Scope_long(_ctx: any): void {
+    this.push('tail(Scope)', []);
+  }
+
+  // ── Ordering step and by() modulator ─────────────────────────────────────
+
+  visitTraversalMethod_order_Empty(ctx: any): void {
+    this.push('order', []);
+  }
+
+  visitTraversalMethod_order_Scope(_ctx: any): void {
+    this.push('order(Scope)', []);
+  }
+
+  /** by(String key) — sort by property value ascending */
+  visitTraversalMethod_by_String(ctx: any): void {
+    this.push('by', [this.recoverStringLiteral(ctx.stringLiteral()), 'asc']);
+  }
+
+  /** by(String key, Order) — sort by property value with direction */
+  visitTraversalMethod_by_String_Comparator(ctx: any): void {
+    const dir = ctx.traversalComparator().traversalOrder().getText();
+    this.push('by', [this.recoverStringLiteral(ctx.stringLiteral()), dir]);
+  }
+
+  /** by(T token) — sort by T.id or T.label */
+  visitTraversalMethod_by_T(ctx: any): void {
+    this.push('by', [ctx.traversalT().getText(), 'asc']);
+  }
+
+  /** by() — natural ascending sort */
+  visitTraversalMethod_by_Empty(_ctx: any): void {
+    this.push('by', [null, 'asc']);
+  }
+
+  /** by(Order) — natural sort with explicit direction */
+  visitTraversalMethod_by_Comparator(ctx: any): void {
+    const dir = ctx.traversalComparator().traversalOrder().getText();
+    this.push('by', [null, dir]);
+  }
+
+  /** by(Order) using traversalOrder directly */
+  visitTraversalMethod_by_Order(ctx: any): void {
+    const dir = ctx.traversalOrder().getText();
+    this.push('by', [null, dir]);
+  }
+
+  visitTraversalMethod_by_Traversal(_ctx: any): void { this.push('by(Traversal)', []); }
+  visitTraversalMethod_by_Traversal_Comparator(_ctx: any): void { this.push('by(Traversal)', []); }
+
+  /** by(T, Order) — T.id, T.label etc. with a direction, routed through traversalFunction */
+  visitTraversalMethod_by_Function(ctx: any): void {
+    const tCtx = ctx.traversalFunction().traversalT?.();
+    if (tCtx) {
+      this.push('by', [tCtx.getText(), 'asc']);
+    } else {
+      this.push('by(Function)', []);
+    }
+  }
+
+  visitTraversalMethod_by_Function_Comparator(ctx: any): void {
+    const tCtx = ctx.traversalFunction().traversalT?.();
+    if (tCtx) {
+      const dir = ctx.traversalComparator().traversalOrder().getText();
+      this.push('by', [tCtx.getText(), dir]);
+    } else {
+      this.push('by(Function)', []);
+    }
+  }
+
+  // ── Mutation steps ────────────────────────────────────────────────────────
+
+  visitTraversalMethod_addV_Empty(ctx: any): void {
+    this.push('addV', []);
+  }
+
+  visitTraversalMethod_addV_String(ctx: any): void {
+    this.push('addV', [this.recoverStringArg(ctx.stringArgument())]);
+  }
+
+  visitTraversalMethod_addV_Traversal(ctx: any): void {
+    this.push('addV', [this.visitNestedTraversalAsSubPipeline(ctx.nestedTraversal())]);
+  }
+
+  visitTraversalMethod_addE_String(ctx: any): void {
+    this.push('addE', [this.recoverStringArg(ctx.stringArgument())]);
+  }
+
+  visitTraversalMethod_addE_Traversal(_ctx: any): void {
+    // addE(Traversal) is not supported in Tiny Gremlin — push an unsupported step name
+    // so the validation pass rejects it with a clear LocalExecutionError.
+    this.push('addE(Traversal)', []);
+  }
+
+  // property(key, value) — the supported form
+  visitTraversalMethod_property_Object_Object_Object(ctx: any): void {
+    this.push('property', [
+      this.recoverGenericLiteral(ctx.genericLiteral()),
+      this.recoverGenericArg(ctx.genericArgument()),
+    ]);
+  }
+
+  // from(traversal) modulator
+  visitTraversalMethod_from_Traversal(ctx: any): void {
+    this.push('from', [this.visitNestedTraversalAsSubPipeline(ctx.nestedTraversal())]);
+  }
+
+  // from(string) modulator — label reference, treated as-is
+  visitTraversalMethod_from_String(ctx: any): void {
+    this.push('from', [this.recoverStringLiteral(ctx.stringLiteral())]);
+  }
+
+  // to(traversal) modulator
+  visitTraversalMethod_to_Traversal(ctx: any): void {
+    this.push('to', [this.visitNestedTraversalAsSubPipeline(ctx.nestedTraversal())]);
+  }
+
+  // to(string) modulator
+  visitTraversalMethod_to_String(ctx: any): void {
+    this.push('to', [this.recoverStringLiteral(ctx.stringLiteral())]);
+  }
+
+  // ── Argument recovery helpers ─────────────────────────────────────────────
+
+  private recoverStringLiteral(ctx: any): string {
+    if (ctx == null) return '';
+    const text: string = ctx.getText();
+    // Strip surrounding quotes (' or ") and unescape escaped quotes
+    if ((text.startsWith("'") && text.endsWith("'")) ||
+        (text.startsWith('"') && text.endsWith('"'))) {
+      return text.slice(1, -1)
+        .replace(/\\'/g, "'")
+        .replace(/\\"/g, '"')
+        .replace(/\\\\/g, '\\');
+    }
+    return text;
+  }
+
+  private recoverStringNullableLiteral(ctx: any): string | null {
+    if (ctx == null) return null;
+    const text: string = ctx.getText();
+    if (text === 'null') return null;
+    return this.recoverStringLiteral(ctx.stringLiteral?.() ?? ctx);
+  }
+
+  private recoverStringArg(ctx: any): string {
+    if (ctx == null) return '';
+    const lit = ctx.stringLiteral?.();
+    return lit ? this.recoverStringLiteral(lit) : ctx.getText();
+  }
+
+  private recoverStringNullableArgument(ctx: any): string | null {
+    if (ctx == null) return null;
+    const lit = ctx.stringNullableLiteral?.();
+    return lit ? this.recoverStringNullableLiteral(lit) : ctx.getText();
+  }
+
+  private recoverStringNullableVarargs(ctx: any): Arg[] {
+    if (ctx == null) return [];
+    const items = ctx.stringNullableArgument?.() ?? [];
+    return (Array.isArray(items) ? items : [items])
+      .map((a: any) => this.recoverStringNullableArgument(a));
+  }
+
+  /** Recover args from a stringNullableLiteralVarargs context (used by values, valueMap, elementMap). */
+  private recoverStringNullableLiteralVarargs(ctx: any): Arg[] {
+    if (ctx == null) return [];
+    const items = ctx.stringNullableLiteral?.() ?? [];
+    return (Array.isArray(items) ? items : [items])
+      .map((a: any) => this.recoverStringNullableLiteral(a));
+  }
+
+  private recoverIntegerArg(ctx: any): number {
+    if (ctx == null) return 0;
+    const lit = ctx.integerLiteral?.();
+    if (lit) return this.recoverIntegerLiteral(lit);
+    return parseInt(ctx.getText(), 10);
+  }
+
+  private recoverIntegerLiteral(ctx: any): number {
+    const text: string = ctx.getText().toLowerCase();
+    // Strip type suffix: L, S, B
+    const stripped = text.replace(/[lsb]$/, '');
+    return parseInt(stripped, 10);
+  }
+
+  private recoverFloatLiteral(ctx: any): number {
+    const text: string = ctx.getText().toLowerCase();
+    if (text === '+infinity') return Infinity;
+    if (text === '-infinity') return -Infinity;
+    if (text === 'nan') return NaN;
+    return parseFloat(text.replace(/[fd]$/, ''));
+  }
+
+  private recoverGenericLiteral(ctx: any): Arg {
+    if (ctx == null) return null;
+    const numeric = ctx.numericLiteral?.();
+    if (numeric) {
+      const int = numeric.integerLiteral?.();
+      if (int) return this.recoverIntegerLiteral(int);
+      const float = numeric.floatLiteral?.();
+      if (float) return this.recoverFloatLiteral(float);
+    }
+    const bool = ctx.booleanLiteral?.();
+    if (bool) return bool.getText() === 'true';
+    const str = ctx.stringLiteral?.();
+    if (str) return this.recoverStringLiteral(str);
+    const nil = ctx.nullLiteral?.();
+    if (nil) return null;
+    // Nested traversal as a generic literal
+    const nested = ctx.nestedTraversal?.();
+    if (nested) return this.visitNestedTraversalAsSubPipeline(nested);
+    // Collection literal [a, b, c]
+    const coll = ctx.genericCollectionLiteral?.();
+    if (coll) return coll.genericLiteral().map((gl: any) => this.recoverGenericLiteral(gl));
+    return ctx.getText();
+  }
+
+  private recoverGenericArg(ctx: any): Arg {
+    if (ctx == null) return null;
+    const lit = ctx.genericLiteral?.();
+    if (lit) return this.recoverGenericLiteral(lit);
+    return ctx.getText();
+  }
+
+  private recoverGenericVarargs(ctx: any): Arg[] {
+    if (ctx == null) return [];
+    const items = ctx.genericArgument?.() ?? [];
+    return (Array.isArray(items) ? items : [items])
+      .map((a: any) => this.recoverGenericArg(a));
+  }
+
+  private recoverPredicate(ctx: any): P | TextP {
+    if (ctx == null) return P.eq(null);
+    const name: string = ctx.constructor?.name ?? '';
+
+    // Compound predicates: predicate DOT and/or LPAREN predicate RPAREN (6 children)
+    if (ctx.getChildCount?.() === 6) {
+      const op = ctx.getChild(2)?.getText();
+      if (op === 'and' || op === 'or') {
+        const left = this.recoverPredicate(ctx.getChild(0));
+        const right = this.recoverPredicate(ctx.getChild(4));
+        return op === 'and' ? left.and(right) : left.or(right);
+      }
+    }
+
+    // TextP string predicates — checked BEFORE 'not' to avoid prefix collision
+    if (name.includes('Predicate_notContaining'))
+      return TextP.notContaining(this.recoverStringArg(ctx.stringArgument()) as string);
+    if (name.includes('Predicate_notStartingWith'))
+      return TextP.notStartingWith(this.recoverStringArg(ctx.stringArgument()) as string);
+    if (name.includes('Predicate_notEndingWith'))
+      return TextP.notEndingWith(this.recoverStringArg(ctx.stringArgument()) as string);
+    if (name.includes('Predicate_notRegex'))
+      return (TextP as any).notRegex(this.recoverStringArg(ctx.stringArgument()) as string);
+    if (name.includes('Predicate_containing'))
+      return TextP.containing(this.recoverStringArg(ctx.stringArgument()) as string);
+    if (name.includes('Predicate_startingWith'))
+      return TextP.startingWith(this.recoverStringArg(ctx.stringArgument()) as string);
+    if (name.includes('Predicate_endingWith'))
+      return TextP.endingWith(this.recoverStringArg(ctx.stringArgument()) as string);
+    if (name.includes('Predicate_regex'))
+      return (TextP as any).regex(this.recoverStringArg(ctx.stringArgument()) as string);
+    // P numeric predicates
+    if (name.includes('Predicate_eq')) return P.eq(this.recoverGenericArg(ctx.genericArgument()));
+    if (name.includes('Predicate_neq')) return P.neq(this.recoverGenericArg(ctx.genericArgument()));
+    if (name.includes('Predicate_lte')) return P.lte(this.recoverGenericArg(ctx.genericArgument()));
+    if (name.includes('Predicate_lt')) return P.lt(this.recoverGenericArg(ctx.genericArgument()));
+    if (name.includes('Predicate_gte')) return P.gte(this.recoverGenericArg(ctx.genericArgument()));
+    if (name.includes('Predicate_gt')) return P.gt(this.recoverGenericArg(ctx.genericArgument()));
+    if (name.includes('Predicate_between')) {
+      const args = ctx.genericArgument();
+      return P.between(this.recoverGenericArg(args[0]), this.recoverGenericArg(args[1]));
+    }
+    if (name.includes('Predicate_within')) {
+      const vals = this.recoverGenericVarargs(ctx.genericArgumentVarargs?.());
+      return (P as any).within(...vals);
+    }
+    if (name.includes('Predicate_without')) {
+      const vals = this.recoverGenericVarargs(ctx.genericArgumentVarargs?.());
+      return (P as any).without(...vals);
+    }
+    if (name.includes('Predicate_inside')) {
+      const args = ctx.genericArgument();
+      return (P as any).inside(this.recoverGenericArg(args[0]), this.recoverGenericArg(args[1]));
+    }
+    if (name.includes('Predicate_outside')) {
+      const args = ctx.genericArgument();
+      return (P as any).outside(this.recoverGenericArg(args[0]), this.recoverGenericArg(args[1]));
+    }
+    if (name.includes('Predicate_not')) return P.not(this.recoverPredicate(ctx.traversalPredicate()));
+
+    // Single-child wrapper: visit the child
+    if (ctx.getChildCount?.() === 1) return this.recoverPredicate(ctx.getChild(0));
+
+    return P.eq(null);
+  }
+
+  // ── Step accumulator ──────────────────────────────────────────────────────
+
+  private push(name: string, args: Arg[]): void {
+    this.steps.push({ name, args });
+  }
+}
diff --git a/gremlin-js/gremlin-javascript/lib/process/anonymous-traversal.ts b/gremlin-js/gremlin-javascript/lib/process/anonymous-traversal.ts
index 5920c48dc9..699e2fc03b 100644
--- a/gremlin-js/gremlin-javascript/lib/process/anonymous-traversal.ts
+++ b/gremlin-js/gremlin-javascript/lib/process/anonymous-traversal.ts
@@ -18,6 +18,7 @@
  */
 
 import { RemoteConnection, RemoteStrategy } from '../driver/remote-connection.js';
+import { LocalGraphConnection } from '../driver/local-graph-connection.js';
 import { Graph } from '../structure/graph.js';
 import { GraphTraversalSource, GraphTraversal } from './graph-traversal.js';
 import { TraversalStrategies } from './traversal-strategy.js';
@@ -51,12 +52,16 @@ export default class AnonymousTraversalSource {
   }
 
   /**
-   * Creates a {@link GraphTraversalSource} binding a {@link RemoteConnection} to a remote {@link Graph} instances as its
-   * reference so that traversals spawned from it will execute over that reference.
-   * @param {RemoteConnection} connection
+   * Creates a {@link GraphTraversalSource} bound to a connection or a local {@link Graph}.
+   * When passed a {@link Graph} instance, execution runs locally via Tiny Gremlin.
+   * When passed a {@link RemoteConnection}, traversals are submitted to a remote server.
+   * @param {RemoteConnection | Graph} connectionOrGraph
    * @return {GraphTraversalSource}
    */
-  with_(connection: RemoteConnection) {
+  with_(connectionOrGraph: RemoteConnection | Graph) {
+    const connection = connectionOrGraph instanceof Graph
+      ? new LocalGraphConnection(connectionOrGraph)
+      : connectionOrGraph;
     const traversalStrategies = new TraversalStrategies();
     traversalStrategies.addStrategy(new RemoteStrategy(connection));
     const gl = new GremlinLang();
diff --git a/gremlin-js/gremlin-javascript/lib/process/local/LocalExecutionError.ts b/gremlin-js/gremlin-javascript/lib/process/local/LocalExecutionError.ts
new file mode 100644
index 0000000000..9658c34249
--- /dev/null
+++ b/gremlin-js/gremlin-javascript/lib/process/local/LocalExecutionError.ts
@@ -0,0 +1,29 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/**
+ * Thrown when a Gremlin traversal contains a step not supported by the
+ * Tiny Gremlin local execution subset.
+ */
+export class LocalExecutionError extends Error {
+  constructor(message: string) {
+    super(message);
+    this.name = 'LocalExecutionError';
+  }
+}
diff --git a/gremlin-js/gremlin-javascript/lib/process/local/LocalExecutor.ts b/gremlin-js/gremlin-javascript/lib/process/local/LocalExecutor.ts
new file mode 100644
index 0000000000..ee4c04dc63
--- /dev/null
+++ b/gremlin-js/gremlin-javascript/lib/process/local/LocalExecutor.ts
@@ -0,0 +1,248 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import type { Arg, Pipeline, StepDescriptor } from './types.js';
+import { defaultPasses, type OptimizationPass } from './passes.js';
+import { LocalExecutionError } from './LocalExecutionError.js';
+import { Graph, Vertex, Edge } from '../../structure/graph.js';
+import {
+  wrap, getValue, type StreamItem,
+  stepOut, stepIn, stepBoth, stepOutE, stepInE, stepBothE, stepOutV, stepInV, stepOtherV,
+  stepHas, stepHasId, stepHasLabel, stepHasNot,
+  stepId, stepLabel, stepValue, stepKey, stepValues, stepValueMap, stepElementMap,
+  stepPath,
+  stepLimit, stepRange, stepSkip, stepTail, stepOrder,
+  stepAddV, stepAddEWithModulators, stepProperty,
+} from './steps.js';
+
+type StepFn = (source: Iterable<StreamItem>, args: Arg[], graph: Graph, trackPaths: boolean) => Generator<StreamItem>;
+
+/**
+ * Executes a Tiny Gremlin Pipeline against a local in-memory Graph, returning
+ * an AsyncGenerator that yields one result element at a time.
+ */
+export class LocalExecutor {
+  private readonly passes: OptimizationPass[];
+
+  constructor(passes: OptimizationPass[] = defaultPasses) {
+    this.passes = passes;
+  }
+
+  execute(pipeline: Pipeline, graph: Graph): AsyncGenerator<any> {
+    const optimized = this.passes.reduce((p, pass) => pass(p), pipeline);
+    const folded = this.foldModulators(optimized);
+    const trackPaths = folded.some(s => s.name === 'path' || s.name === 'otherV');
+    const stream = this.buildChain(folded, graph, trackPaths);
+    return this.toAsync(stream, trackPaths);
+  }
+
+  /**
+   * Folds 'from' and 'to' modulator steps that follow an 'addE' step into a
+   * composite descriptor so the addE executor has all modulator args available.
+   */
+  private foldModulators(pipeline: Pipeline): Pipeline {
+    const result: StepDescriptor[] = [];
+    let i = 0;
+    while (i < pipeline.length) {
+      const step = pipeline[i];
+      if (step.name === 'addE') {
+        let fromArgs: Arg[] | null = null;
+        let toArgs: Arg[] | null = null;
+        let j = i + 1;
+        while (j < pipeline.length && (pipeline[j].name === 'from' || pipeline[j].name === 'to')) {
+          if (pipeline[j].name === 'from') fromArgs = pipeline[j].args[0] as unknown as Arg[];
+          else toArgs = pipeline[j].args[0] as unknown as Arg[];
+          j++;
+        }
+        result.push({ name: 'addE', args: [step.args[0], fromArgs as unknown as Arg, toArgs as unknown as Arg] });
+        i = j;
+      } else if (step.name === 'order') {
+        // Fold a single following by() modulator into the order step args
+        const next = pipeline[i + 1];
+        if (next?.name === 'by') {
+          result.push({ name: 'order', args: next.args }); // args = [key, direction]
+          i += 2;
+        } else {
+          result.push(step);
+          i++;
+        }
+      } else {
+        result.push(step);
+        i++;
+      }
+    }
+    return result;
+  }
+
+  private buildChain(pipeline: Pipeline, graph: Graph, trackPaths: boolean): Iterable<StreamItem> {
+    if (pipeline.length === 0) return [];
+
+    const [source, ...rest] = pipeline;
+    let stream: Iterable<StreamItem> = this.applySourceStep(source, graph, trackPaths);
+
+    for (const step of rest) {
+      stream = this.applyStep(step, stream, graph, trackPaths);
+    }
+    return stream;
+  }
+
+  private applySourceStep(step: StepDescriptor, graph: Graph, trackPaths: boolean): Iterable<StreamItem> {
+    switch (step.name) {
+      case 'V': return sourceV(step.args, graph, trackPaths);
+      case 'E': return sourceE(step.args, graph, trackPaths);
+      case 'addV': return stepAddV([null], step.args, graph, trackPaths);
+      case 'addE': return this.applyAddE([null], step, graph, trackPaths);
+      default:
+        throw new LocalExecutionError(`'${step.name}' cannot be used as a source step`);
+    }
+  }
+
+  private applyStep(step: StepDescriptor, source: Iterable<StreamItem>, graph: Graph, trackPaths: boolean): Iterable<StreamItem> {
+    if (step.name === 'addE') return this.applyAddE(source, step, graph, trackPaths);
+    const fn = STEP_REGISTRY.get(step.name);
+    if (!fn) throw new LocalExecutionError(`No implementation for step '${step.name}'`);
+    return fn(source, step.args, graph, trackPaths);
+  }
+
+  private applyAddE(
+    source: Iterable<StreamItem>,
+    step: StepDescriptor,
+    graph: Graph,
+    trackPaths: boolean,
+  ): Iterable<StreamItem> {
+    const label = step.args[0] as string;
+    const fromPipeline = step.args[1] as Arg[] | null;
+    const toPipeline = step.args[2] as Arg[] | null;
+    return stepAddEWithModulators(
+      source, label, fromPipeline, toPipeline, graph, trackPaths,
+      (subPipeline, g) => this.executeSubPipeline(subPipeline as unknown as Pipeline, g),
+    );
+  }
+
+  private executeSubPipeline(pipeline: Pipeline, graph: Graph): any {
+    const stream = this.buildChain(pipeline, graph, false);
+    for (const item of stream) return item;
+    return null;
+  }
+
+  private async *toAsync(stream: Iterable<StreamItem>, trackPaths: boolean): AsyncGenerator<any> {
+    for (const item of stream) {
+      const value = trackPaths ? getValue(item, true) : item;
+      // Yield a shallow copy of Vertex/Edge so callers that mutate
+      // result objects (e.g. removeProperties) don't corrupt the graph.
+      yield shallowCopyElement(value);
+    }
+  }
+}
+
+// ── Result copying ────────────────────────────────────────────────────────────
+
+/** Shallow-copies Vertex/Edge/Path so callers can mutate results without corrupting the graph. */
+function shallowCopyElement(value: any): any {
+  if (value instanceof Vertex || value instanceof Edge) return Object.assign(Object.create(Object.getPrototypeOf(value)), value);
+  return value;
+}
+
+// ── Source steps ──────────────────────────────────────────────────────────────
+// Mid-traversal V/E: for each incoming traverser, emit matching vertices/edges (cross-product).
+
+function* midSourceV(source: Iterable<StreamItem>, args: Arg[], graph: Graph, trackPaths: boolean): Generator<StreamItem> {
+  for (const _item of source) {
+    yield* sourceV(args, graph, trackPaths);
+  }
+}
+
+function* midSourceE(source: Iterable<StreamItem>, args: Arg[], graph: Graph, trackPaths: boolean): Generator<StreamItem> {
+  for (const _item of source) {
+    yield* sourceE(args, graph, trackPaths);
+  }
+}
+
+function lookupById<T>(map: Map<any, T>, id: Arg): T | undefined {
+  return map.get(id);
+}
+
+function flattenIds(args: Arg[]): Arg[] {
+  // Single list argument: unroll it, consistent with V([1,2,3]) semantics.
+  // Multiple arguments: treat each as-is — a list is a single ID value, not unrolled.
+  if (args.length === 1 && Array.isArray(args[0])) return args[0] as unknown as Arg[];
+  return args;
+}
+
+function* sourceV(args: Arg[], graph: Graph, trackPaths: boolean): Generator<StreamItem> {
+  const ids = flattenIds(args);
+  if (ids.length === 0) {
+    for (const v of graph.vertices.values()) yield wrap(v, [], trackPaths);
+  } else {
+    for (const id of ids) {
+      if (id === null || id === undefined) continue;
+      const v = lookupById(graph.vertices, id);
+      if (v !== undefined) yield wrap(v, [], trackPaths);
+    }
+  }
+}
+
+function* sourceE(args: Arg[], graph: Graph, trackPaths: boolean): Generator<StreamItem> {
+  const ids = flattenIds(args);
+  if (ids.length === 0) {
+    for (const e of graph.edges.values()) yield wrap(e, [], trackPaths);
+  } else {
+    for (const id of ids) {
+      if (id === null || id === undefined) continue;
+      const e = lookupById(graph.edges, id);
+      if (e !== undefined) yield wrap(e, [], trackPaths);
+    }
+  }
+}
+
+// ── Step dispatch table ───────────────────────────────────────────────────────
+
+const STEP_REGISTRY = new Map<string, StepFn>([
+  // Mid-traversal V/E: for each incoming element produce matching vertices/edges (cross-product)
+  ['V',           (source, args, graph, tp) => midSourceV(source, args, graph, tp)],
+  ['E',           (source, args, graph, tp) => midSourceE(source, args, graph, tp)],
+  ['out',         stepOut],
+  ['in',          stepIn],
+  ['both',        stepBoth],
+  ['outE',        stepOutE],
+  ['inE',         stepInE],
+  ['bothE',       stepBothE],
+  ['outV',        stepOutV],
+  ['inV',         stepInV],
+  ['otherV',      stepOtherV],
+  ['has',         stepHas],
+  ['hasId',       stepHasId],
+  ['hasLabel',    stepHasLabel],
+  ['hasNot',      stepHasNot],
+  ['id',          stepId],
+  ['label',       stepLabel],
+  ['value',       stepValue],
+  ['key',         stepKey],
+  ['values',      stepValues],
+  ['valueMap',    stepValueMap],
+  ['elementMap',  stepElementMap],
+  ['path',        stepPath],
+  ['limit',       stepLimit],
+  ['range',       stepRange],
+  ['skip',        stepSkip],
+  ['tail',        stepTail],
+  ['order',       stepOrder],
+  ['addV',        stepAddV],
+  ['property',    stepProperty],
+]);
diff --git a/gremlin-js/gremlin-javascript/lib/process/local/passes.ts b/gremlin-js/gremlin-javascript/lib/process/local/passes.ts
new file mode 100644
index 0000000000..ce7635263a
--- /dev/null
+++ b/gremlin-js/gremlin-javascript/lib/process/local/passes.ts
@@ -0,0 +1,87 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import type { Arg, Pipeline, StepDescriptor } from './types.js';
+import { LocalExecutionError } from './LocalExecutionError.js';
+
+export type OptimizationPass = (pipeline: Pipeline) => Pipeline;
+
+/** Steps supported by the Tiny Gremlin local executor. */
+const SUPPORTED_STEPS = new Set<string>([
+  'V', 'E',
+  'out', 'in', 'both', 'outE', 'inE', 'bothE', 'outV', 'inV', 'otherV',
+  'has', 'hasId', 'hasLabel', 'hasNot',
+  'id', 'label', 'values', 'valueMap', 'elementMap', 'value', 'key',
+  'path',
+  'limit', 'range', 'skip', 'tail',
+  'order', 'by',
+  'addV', 'addE', 'property',
+  'from', 'to',
+]);
+
+/**
+ * Validation pass — runs first. Throws LocalExecutionError for any step not
+ * in the Tiny Gremlin subset, including steps inside nested sub-pipelines.
+ */
+export function validationPass(pipeline: Pipeline): Pipeline {
+  for (const step of pipeline) {
+    if (!SUPPORTED_STEPS.has(step.name)) {
+      throw new LocalExecutionError(
+        `'${step.name}' is not supported for Tiny Gremlin local execution`,
+      );
+    }
+    for (const arg of step.args) {
+      if (Array.isArray(arg) && arg.length > 0 && arg[0] !== null && typeof arg[0] === 'object' && 'name' in arg[0]) {
+        validationPass(arg as Pipeline);
+      }
+    }
+  }
+  return pipeline;
+}
+
+/**
+ * HasId folding pass — collapses V().hasId(id) → V(id) and E().hasId(id) → E(id),
+ * enabling O(1) direct map lookup instead of full iteration.
+ */
+export function hasIdFoldingPass(pipeline: Pipeline): Pipeline {
+  const result: StepDescriptor[] = [];
+  let i = 0;
+  while (i < pipeline.length) {
+    const step = pipeline[i];
+    const next = pipeline[i + 1];
+    if (
+      (step.name === 'V' || step.name === 'E') &&
+      step.args.length === 0 &&
+      next?.name === 'hasId'
+    ) {
+      result.push({ name: step.name, args: next.args });
+      i += 2;
+    } else {
+      result.push(step);
+      i++;
+    }
+  }
+  return result;
+}
+
+/** Default optimization passes applied before execution. */
+export const defaultPasses: OptimizationPass[] = [
+  validationPass,
+  hasIdFoldingPass,
+];
diff --git a/gremlin-js/gremlin-javascript/lib/process/local/steps.ts b/gremlin-js/gremlin-javascript/lib/process/local/steps.ts
new file mode 100644
index 0000000000..7ae0700683
--- /dev/null
+++ b/gremlin-js/gremlin-javascript/lib/process/local/steps.ts
@@ -0,0 +1,730 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { P, TextP, t, direction } from '../traversal.js';
+import { Graph, Vertex, Edge, VertexProperty, Property, Path } from '../../structure/graph.js';
+import type { Arg } from './types.js';
+
+// ── Traverser helpers ─────────────────────────────────────────────────────────
+
+export type StreamItem = any;
+
+export function wrap(value: any, prevPath: PathEntry[], trackPaths: boolean): StreamItem {
+  if (!trackPaths) return value;
+  return { value, path: [...prevPath, { labels: [], object: value }] };
+}
+
+export function getValue(item: StreamItem, trackPaths: boolean): any {
+  return trackPaths ? item.value : item;
+}
+
+export function getPath(item: StreamItem, trackPaths: boolean): PathEntry[] {
+  return trackPaths ? (item.path ?? []) : [];
+}
+
+export interface PathEntry {
+  labels: string[];
+  object: any;
+}
+
+// ── Property lookup helpers ───────────────────────────────────────────────────
+
+/** Returns the property key regardless of whether it's a Property or VertexProperty. */
+function propKey(p: Property | VertexProperty): string {
+  return p instanceof VertexProperty ? p.label : p.key;
+}
+
+/** Returns all properties of an element as a flat list. */
+function allProps(el: Vertex | Edge): Array<Property | VertexProperty> {
+  return (el.properties ?? []) as Array<Property | VertexProperty>;
+}
+
+/** Returns all values for a given key on an element (vertex properties are multi-valued). */
+function getValues(el: Vertex | Edge, key: string): any[] {
+  return allProps(el)
+    .filter(p => propKey(p) === key)
+    .map(p => (p as any).value);
+}
+
+/** Returns true when the element has at least one property with the given key. */
+function hasKey(el: Vertex | Edge, key: string): boolean {
+  return allProps(el).some(p => propKey(p) === key);
+}
+
+// ── Predicate evaluation ──────────────────────────────────────────────────────
+
+export function evaluatePredicate(pred: P | TextP, value: any): boolean {
+  const op = pred.operator;
+  const v = pred.value;
+  const other = pred.other;
+
+  switch (op) {
+    case 'eq':  { const c = compareForPredicate(value, v); return c !== null && c === 0; }
+    case 'neq': { const c = compareForPredicate(value, v); return c !== null ? c !== 0 : value !== v; }
+    case 'lt':  { const c = compareForPredicate(value, v); return c !== null && c < 0; }
+    case 'lte': { const c = compareForPredicate(value, v); return c !== null && c <= 0; }
+    case 'gt':  { const c = compareForPredicate(value, v); return c !== null && c > 0; }
+    case 'gte': { const c = compareForPredicate(value, v); return c !== null && c >= 0; }
+    case 'between': {
+      const clo = compareForPredicate(value, v);
+      const chi = compareForPredicate(value, other);
+      return clo !== null && chi !== null && clo >= 0 && chi < 0;
+    }
+    case 'inside': {
+      const clo = compareForPredicate(value, v);
+      const chi = compareForPredicate(value, other);
+      return clo !== null && chi !== null && clo > 0 && chi < 0;
+    }
+    case 'outside': {
+      const clo = compareForPredicate(value, v);
+      const chi = compareForPredicate(value, other);
+      return (clo !== null && clo < 0) || (chi !== null && chi > 0);
+    }
+    case 'within':  return (Array.isArray(v) ? v : [v]).includes(value);
+    case 'without': return !(Array.isArray(v) ? v : [v]).includes(value);
+    case 'not':     return !evaluatePredicate(v as P, value);
+    case 'and':     return evaluatePredicate(v as P, value) && evaluatePredicate(other as P, value);
+    case 'or':      return evaluatePredicate(v as P, value) || evaluatePredicate(other as P, value);
+    case 'containing':       return typeof value === 'string' && value.includes(v as string);
+    case 'notContaining':    return typeof value === 'string' && !value.includes(v as string);
+    case 'startingWith':     return typeof value === 'string' && value.startsWith(v as string);
+    case 'notStartingWith':  return typeof value === 'string' && !value.startsWith(v as string);
+    case 'endingWith':       return typeof value === 'string' && value.endsWith(v as string);
+    case 'notEndingWith':    return typeof value === 'string' && !value.endsWith(v as string);
+    case 'regex':    return typeof value === 'string' && new RegExp(v as string, 'u').test(value);
+    case 'notRegex': return typeof value === 'string' && !new RegExp(v as string, 'u').test(value);
+    default: return false;
+  }
+}
+
+// ── Navigation steps ──────────────────────────────────────────────────────────
+
+export function* stepOut(
+  source: Iterable<StreamItem>, args: Arg[], graph: Graph, trackPaths: boolean,
+): Generator<StreamItem> {
+  const labels = args as string[];
+  for (const item of source) {
+    const v: Vertex = getValue(item, trackPaths);
+    const edges = graph.outEdges.get(v.id) ?? [];
+    for (const e of edges) {
+      if (labels.length === 0 || labels.includes(e.label)) {
+        yield wrap(e.inV, getPath(item, trackPaths), trackPaths);
+      }
+    }
+  }
+}
+
+export function* stepIn(
+  source: Iterable<StreamItem>, args: Arg[], graph: Graph, trackPaths: boolean,
+): Generator<StreamItem> {
+  const labels = args as string[];
+  for (const item of source) {
+    const v: Vertex = getValue(item, trackPaths);
+    const edges = graph.inEdges.get(v.id) ?? [];
+    for (const e of edges) {
+      if (labels.length === 0 || labels.includes(e.label)) {
+        yield wrap(e.outV, getPath(item, trackPaths), trackPaths);
+      }
+    }
+  }
+}
+
+export function* stepBoth(
+  source: Iterable<StreamItem>, args: Arg[], graph: Graph, trackPaths: boolean,
+): Generator<StreamItem> {
+  const buf = [...source];
+  yield* stepOut(buf, args, graph, trackPaths);
+  yield* stepIn(buf, args, graph, trackPaths);
+}
+
+export function* stepOutE(
+  source: Iterable<StreamItem>, args: Arg[], graph: Graph, trackPaths: boolean,
+): Generator<StreamItem> {
+  const labels = args as string[];
+  for (const item of source) {
+    const v: Vertex = getValue(item, trackPaths);
+    const edges = graph.outEdges.get(v.id) ?? [];
+    for (const e of edges) {
+      if (labels.length === 0 || labels.includes(e.label)) {
+        yield wrap(e, getPath(item, trackPaths), trackPaths);
+      }
+    }
+  }
+}
+
+export function* stepInE(
+  source: Iterable<StreamItem>, args: Arg[], graph: Graph, trackPaths: boolean,
+): Generator<StreamItem> {
+  const labels = args as string[];
+  for (const item of source) {
+    const v: Vertex = getValue(item, trackPaths);
+    const edges = graph.inEdges.get(v.id) ?? [];
+    for (const e of edges) {
+      if (labels.length === 0 || labels.includes(e.label)) {
+        yield wrap(e, getPath(item, trackPaths), trackPaths);
+      }
+    }
+  }
+}
+
+export function* stepBothE(
+  source: Iterable<StreamItem>, args: Arg[], graph: Graph, trackPaths: boolean,
+): Generator<StreamItem> {
+  const buf = [...source];  // already buffered correctly
+  yield* stepOutE(buf, args, graph, trackPaths);
+  yield* stepInE(buf, args, graph, trackPaths);
+}
+
+export function* stepOutV(
+  source: Iterable<StreamItem>, args: Arg[], graph: Graph, trackPaths: boolean,
+): Generator<StreamItem> {
+  for (const item of source) {
+    const e: Edge = getValue(item, trackPaths);
+    yield wrap(e.outV, getPath(item, trackPaths), trackPaths);
+  }
+}
+
+export function* stepInV(
+  source: Iterable<StreamItem>, args: Arg[], graph: Graph, trackPaths: boolean,
+): Generator<StreamItem> {
+  for (const item of source) {
+    const e: Edge = getValue(item, trackPaths);
+    yield wrap(e.inV, getPath(item, trackPaths), trackPaths);
+  }
+}
+
+export function* stepOtherV(
+  source: Iterable<StreamItem>, args: Arg[], graph: Graph, trackPaths: boolean,
+): Generator<StreamItem> {
+  // trackPaths is always true when otherV is present (forced in LocalExecutor)
+  for (const item of source) {
+    const e: Edge = getValue(item, trackPaths);
+    const path = getPath(item, trackPaths);
+    const prevEntry = path.length >= 2 ? path[path.length - 2] : null;
+    const prevId = prevEntry?.object?.id;
+    const result = prevId === e.outV.id ? e.inV : e.outV;
+    yield wrap(result, path, trackPaths);
+  }
+}
+
+// ── Filter steps ──────────────────────────────────────────────────────────────
+
+export function* stepHas(
+  source: Iterable<StreamItem>, args: Arg[], graph: Graph, trackPaths: boolean,
+): Generator<StreamItem> {
+  for (const item of source) {
+    const el = getValue(item, trackPaths);
+    if (matchesHas(el, args)) yield item;
+  }
+}
+
+function matchesHas(el: any, args: Arg[]): boolean {
+  if (!(el instanceof Vertex || el instanceof Edge)) return false;
+
+  if (args.length === 1) {
+    const key = args[0] as string;
+    if (key === 'T.id' || key === 'id') return true;
+    if (key === 'T.label' || key === 'label') return true;
+    return hasKey(el, key);
+  }
+
+  if (args.length === 2) {
+    const [key, valOrPred] = args;
+    const keyStr = key as string;
+    let values: any[];
+    if (keyStr === 'T.id' || keyStr === 'id') {
+      values = [el.id];
+    } else if (keyStr === 'T.label' || keyStr === 'label') {
+      values = [el.label];
+    } else {
+      values = getValues(el, keyStr);
+    }
+    if (values.length === 0) return false;
+    if (valOrPred instanceof P || valOrPred instanceof TextP) {
+      return values.some(v => evaluatePredicate(valOrPred, v));
+    }
+    return values.some(v => v === valOrPred);
+  }
+
+  if (args.length === 3) {
+    const [label, key, valOrPred] = args;
+    if (el.label !== label) return false;
+    const values = getValues(el, key as string);
+    if (values.length === 0) return false;
+    if (valOrPred instanceof P || valOrPred instanceof TextP) {
+      return values.some(v => evaluatePredicate(valOrPred as P, v));
+    }
+    return values.some(v => v === valOrPred);
+  }
+
+  return false;
+}
+
+function idMatches(id: any, argId: Arg): boolean {
+  if (argId === null || argId === undefined) return false;
+  return id === argId;
+}
+
+function flattenIdArgs(args: Arg[]): Arg[] {
+  // Single list argument: unroll it, consistent with hasId([1,2,3]) semantics.
+  // Multiple arguments: treat each as-is — a list is a single ID value, not unrolled.
+  if (args.length === 1 && Array.isArray(args[0])) return args[0] as unknown as Arg[];
+  return args;
+}
+
+export function* stepHasId(
+  source: Iterable<StreamItem>, args: Arg[], graph: Graph, trackPaths: boolean,
+): Generator<StreamItem> {
+  const flatArgs = flattenIdArgs(args);
+  for (const item of source) {
+    const el = getValue(item, trackPaths);
+    const id = el?.id;
+    if (flatArgs.length > 0 && flatArgs[0] instanceof P) {
+      if (evaluatePredicate(flatArgs[0] as P, id)) yield item;
+    } else if (flatArgs.some(a => idMatches(id, a))) {
+      yield item;
+    }
+  }
+}
+
+export function* stepHasLabel(
+  source: Iterable<StreamItem>, args: Arg[], graph: Graph, trackPaths: boolean,
+): Generator<StreamItem> {
+  for (const item of source) {
+    const el = getValue(item, trackPaths);
+    const label = el?.label;
+    if (args.length > 0 && args[0] instanceof P) {
+      if (evaluatePredicate(args[0] as P, label)) yield item;
+    } else if (args.length === 0 || args.includes(label)) {
+      yield item;
+    }
+  }
+}
+
+export function* stepHasNot(
+  source: Iterable<StreamItem>, args: Arg[], graph: Graph, trackPaths: boolean,
+): Generator<StreamItem> {
+  const key = args[0] as string;
+  for (const item of source) {
+    const el = getValue(item, trackPaths);
+    if (el instanceof Vertex || el instanceof Edge) {
+      if (!hasKey(el, key)) yield item;
+    }
+  }
+}
+
+export function* stepHasKey(
+  source: Iterable<StreamItem>, args: Arg[], graph: Graph, trackPaths: boolean,
+): Generator<StreamItem> {
+  for (const item of source) {
+    const p = getValue(item, trackPaths);
+    const k = propKey(p);
+    if (args.length === 0) {
+      yield item;
+    } else if (args[0] instanceof P) {
+      if (evaluatePredicate(args[0] as P, k)) yield item;
+    } else if (args.includes(k)) {
+      yield item;
+    }
+  }
+}
+
+export function* stepHasValue(
+  source: Iterable<StreamItem>, args: Arg[], graph: Graph, trackPaths: boolean,
+): Generator<StreamItem> {
+  for (const item of source) {
+    const p = getValue(item, trackPaths);
+    const v = p?.value;
+    if (args.length === 0) {
+      yield item;
+    } else if (args[0] instanceof P) {
+      if (evaluatePredicate(args[0] as P, v)) yield item;
+    } else if (args.some(a => a == v)) {
+      yield item;
+    }
+  }
+}
+
+// ── Value extraction steps ────────────────────────────────────────────────────
+
+export function* stepId(
+  source: Iterable<StreamItem>, args: Arg[], graph: Graph, trackPaths: boolean,
+): Generator<StreamItem> {
+  for (const item of source) {
+    const el = getValue(item, trackPaths);
+    yield wrap(el?.id, getPath(item, trackPaths), trackPaths);
+  }
+}
+
+export function* stepLabel(
+  source: Iterable<StreamItem>, args: Arg[], graph: Graph, trackPaths: boolean,
+): Generator<StreamItem> {
+  for (const item of source) {
+    const el = getValue(item, trackPaths);
+    yield wrap(el?.label, getPath(item, trackPaths), trackPaths);
+  }
+}
+
+export function* stepValue(
+  source: Iterable<StreamItem>, args: Arg[], graph: Graph, trackPaths: boolean,
+): Generator<StreamItem> {
+  for (const item of source) {
+    const p = getValue(item, trackPaths);
+    yield wrap(p?.value, getPath(item, trackPaths), trackPaths);
+  }
+}
+
+export function* stepKey(
+  source: Iterable<StreamItem>, args: Arg[], graph: Graph, trackPaths: boolean,
+): Generator<StreamItem> {
+  for (const item of source) {
+    const p = getValue(item, trackPaths);
+    yield wrap(propKey(p), getPath(item, trackPaths), trackPaths);
+  }
+}
+
+export function* stepValues(
+  source: Iterable<StreamItem>, args: Arg[], graph: Graph, trackPaths: boolean,
+): Generator<StreamItem> {
+  const keys = args as string[];
+  for (const item of source) {
+    const el = getValue(item, trackPaths);
+    const props = allProps(el);
+    for (const p of props) {
+      if (keys.length === 0 || keys.includes(propKey(p))) {
+        yield wrap((p as any).value, getPath(item, trackPaths), trackPaths);
+      }
+    }
+  }
+}
+
+export function* stepProperties(
+  source: Iterable<StreamItem>, args: Arg[], graph: Graph, trackPaths: boolean,
+): Generator<StreamItem> {
+  const keys = args as string[];
+  for (const item of source) {
+    const el = getValue(item, trackPaths);
+    const props = allProps(el);
+    for (const p of props) {
+      if (keys.length === 0 || keys.includes(propKey(p))) {
+        yield wrap(p, getPath(item, trackPaths), trackPaths);
+      }
+    }
+  }
+}
+
+export function* stepValueMap(
+  source: Iterable<StreamItem>, args: Arg[], graph: Graph, trackPaths: boolean,
+): Generator<StreamItem> {
+  // A boolean true as the first arg means includeTokens (valueMap(true, ...))
+  const includeTokens = args.length > 0 && args[0] === true;
+  const keys = (includeTokens ? args.slice(1) : args) as string[];
+  for (const item of source) {
+    const el = getValue(item, trackPaths);
+    const map = new Map<any, any[]>();
+    if (includeTokens) {
+      map.set(t.id, el?.id);
+      map.set(t.label, el?.label);
+    }
+    for (const p of allProps(el)) {
+      const k = propKey(p);
+      if (keys.length === 0 || keys.includes(k)) {
+        if (!map.has(k)) map.set(k, []);
+        map.get(k)!.push((p as any).value);
+      }
+    }
+    yield wrap(map, getPath(item, trackPaths), trackPaths);
+  }
+}
+
+/** Builds the stub vertex map used as Direction.OUT / Direction.IN values in edge elementMap. */
+function vertexStubMap(v: Vertex): Map<any, any> {
+  const m = new Map<any, any>();
+  m.set(t.id, v.id);
+  m.set(t.label, v.label);
+  return m;
+}
+
+export function* stepElementMap(
+  source: Iterable<StreamItem>, args: Arg[], graph: Graph, trackPaths: boolean,
+): Generator<StreamItem> {
+  const keys = args as string[];
+  for (const item of source) {
+    const el = getValue(item, trackPaths);
+    const map = new Map<any, any>();
+    map.set(t.id, el?.id);
+    map.set(t.label, el?.label);
+    if (el instanceof Edge) {
+      // Standard Gremlin edge elementMap includes Direction.OUT and Direction.IN stub maps
+      map.set(direction.out, vertexStubMap(el.outV));
+      map.set(direction.in, vertexStubMap(el.inV));
+    }
+    for (const p of allProps(el)) {
+      const k = propKey(p);
+      if (keys.length === 0 || keys.includes(k)) {
+        map.set(k, (p as any).value);
+      }
+    }
+    yield wrap(map, getPath(item, trackPaths), trackPaths);
+  }
+}
+
+// ── Path step ─────────────────────────────────────────────────────────────────
+
+export function* stepPath(
+  source: Iterable<StreamItem>, args: Arg[], graph: Graph, trackPaths: boolean,
+): Generator<StreamItem> {
+  for (const item of source) {
+    const pathEntries: PathEntry[] = getPath(item, trackPaths);
+    const labels: string[][] = pathEntries.map(e => e.labels);
+    const objects: any[] = pathEntries.map(e => e.object);
+    const pathObj = new Path(labels, objects);
+    // Yield as a traverser so toAsync's trackPaths unwrap produces the Path value correctly.
+    yield trackPaths ? { value: pathObj, path: [] } : pathObj;
+  }
+}
+
+// ── Range steps ───────────────────────────────────────────────────────────────
+
+export function* stepLimit(
+  source: Iterable<StreamItem>, args: Arg[], graph: Graph, trackPaths: boolean,
+): Generator<StreamItem> {
+  const n = args[0] as number;
+  let count = 0;
+  for (const item of source) {
+    if (count >= n) return;
+    yield item;
+    count++;
+  }
+}
+
+export function* stepRange(
+  source: Iterable<StreamItem>, args: Arg[], graph: Graph, trackPaths: boolean,
+): Generator<StreamItem> {
+  const lo = args[0] as number;
+  const hi = args[1] as number;
+  if (lo > hi) throw new Error(`Not a legal range: [${lo}, ${hi}]`);
+  let i = 0;
+  for (const item of source) {
+    if (i >= hi) return;
+    if (i >= lo) yield item;
+    i++;
+  }
+}
+
+export function* stepSkip(
+  source: Iterable<StreamItem>, args: Arg[], graph: Graph, trackPaths: boolean,
+): Generator<StreamItem> {
+  const n = args[0] as number;
+  let i = 0;
+  for (const item of source) {
+    if (i++ >= n) yield item;
+  }
+}
+
+export function* stepTail(
+  source: Iterable<StreamItem>, args: Arg[], graph: Graph, trackPaths: boolean,
+): Generator<StreamItem> {
+  const n = args[0] as number;
+  const buf: StreamItem[] = [];
+  for (const item of source) buf.push(item);
+  yield* buf.slice(Math.max(0, buf.length - n));
+}
+
+// ── Gremlin Orderability semantics ───────────────────────────────────────────
+// Total order across all types per the priority list in gremlin-semantics.asciidoc.
+
+function gremlinTypeOrder(v: any): number {
+  if (v === null || v === undefined) return 1;          // nulltype
+  if (typeof v === 'boolean') return 2;                 // Boolean
+  if (typeof v === 'number' || typeof v === 'bigint') return 3; // Number
+  if (v instanceof Date) return 4;                      // Date
+  if (typeof v === 'string') return 5;                  // String
+  if (v instanceof Vertex) return 7;                    // Vertex
+  if (v instanceof Edge) return 8;                      // Edge
+  if (v instanceof VertexProperty) return 9;            // VertexProperty
+  if (v instanceof Property) return 10;                 // Property
+  if (v instanceof Path) return 11;                     // Path
+  if (v instanceof Set) return 12;                      // Set
+  if (Array.isArray(v)) return 13;                      // List
+  if (v instanceof Map) return 14;                      // Map
+  return 15;                                            // Unknown
+}
+
+function compareNumeric(a: number, b: number): number {
+  // Per orderability: NaN > +Infinity > everything else
+  const aNaN = Number.isNaN(a), bNaN = Number.isNaN(b);
+  if (aNaN && bNaN) return 0;
+  if (aNaN) return 1;
+  if (bNaN) return -1;
+  return a < b ? -1 : a > b ? 1 : 0;
+}
+
+function compareValues(va: any, vb: any): number {
+  const ta = gremlinTypeOrder(va);
+  const tb = gremlinTypeOrder(vb);
+  if (ta !== tb) return ta - tb;
+
+  // Within same type family:
+  if (va === null || va === undefined) return 0;
+  if (typeof va === 'boolean') return (va ? 1 : 0) - (vb ? 1 : 0);
+  if (typeof va === 'number') return compareNumeric(va as number, vb as number);
+  if (typeof va === 'bigint') return va < vb ? -1 : va > vb ? 1 : 0;
+  if (va instanceof Date) return va.getTime() - (vb as Date).getTime();
+  if (typeof va === 'string') return va < vb ? -1 : va > vb ? 1 : 0;
+  // Elements: compare by id
+  if (va instanceof Vertex || va instanceof Edge || va instanceof VertexProperty) {
+    return compareValues((va as any).id, (vb as any).id);
+  }
+  if (va instanceof Property) {
+    const kc = compareValues((va as Property).key, (vb as Property).key);
+    return kc !== 0 ? kc : compareValues((va as Property).value, (vb as Property).value);
+  }
+  if (Array.isArray(va)) {
+    const len = Math.min(va.length, (vb as any[]).length);
+    for (let i = 0; i < len; i++) {
+      const c = compareValues(va[i], (vb as any[])[i]);
+      if (c !== 0) return c;
+    }
+    return va.length - (vb as any[]).length;
+  }
+  // Unknown: class name then toString()
+  const sa = Object.prototype.toString.call(va);
+  const sb = Object.prototype.toString.call(vb);
+  if (sa !== sb) return sa < sb ? -1 : 1;
+  const ts = String(va), tv = String(vb);
+  return ts < tv ? -1 : ts > tv ? 1 : 0;
+}
+
+// ── Gremlin Comparability semantics ──────────────────────────────────────────
+// Used by P.lt/lte/gt/gte: returns null (ERROR) for cross-type or NaN comparisons.
+
+function compareForPredicate(va: any, vb: any): number | null {
+  const ta = gremlinTypeOrder(va);
+  const tb = gremlinTypeOrder(vb);
+  if (ta !== tb) return null; // cross-type: ERROR
+  if (va === null || va === undefined) return 0; // null == null
+  if (typeof va === 'number') {
+    if (Number.isNaN(va) || Number.isNaN(vb)) return null; // NaN: ERROR
+    return compareNumeric(va as number, vb as number);
+  }
+  return compareValues(va, vb);
+}
+
+function getByValue(el: any, key: string | null | undefined): any {
+  if (key == null) return el;
+  if (key === 'T.id' || key === 'id') return el?.id;
+  if (key === 'T.label' || key === 'label') return el?.label;
+  if (el instanceof Vertex || el instanceof Edge) {
+    const vals = getValues(el, key);
+    return vals[0]; // first value for multi-valued properties
+  }
+  return el;
+}
+
+export function* stepOrder(
+  source: Iterable<StreamItem>, args: Arg[], graph: Graph, trackPaths: boolean,
+): Generator<StreamItem> {
+  const key = args[0] as string | null | undefined;
+  const dir = ((args[1] as string) ?? 'asc').split('.').pop()!.toLowerCase();
+  const multiplier = dir === 'desc' ? -1 : 1;
+  const buf: StreamItem[] = [...source];
+  buf.sort((a, b) => {
+    const va = getByValue(getValue(a, trackPaths), key as string | null | undefined);
+    const vb = getByValue(getValue(b, trackPaths), key as string | null | undefined);
+    return multiplier * compareValues(va, vb);
+  });
+  yield* buf;
+}
+
+// ── Mutation steps ────────────────────────────────────────────────────────────
+
+export function* stepAddV(
+  source: Iterable<StreamItem>, args: Arg[], graph: Graph, trackPaths: boolean,
+): Generator<StreamItem> {
+  const label = (args[0] as string | undefined) ?? 'vertex';
+  for (const _item of source) {
+    const id = graph.vertices.size;
+    const v = new Vertex(id, label, []);
+    graph.vertices.set(id, v);
+    yield wrap(v, [], trackPaths);
+  }
+}
+
+export function* stepAddEWithModulators(
+  source: Iterable<StreamItem>,
+  label: string,
+  fromPipeline: Arg[] | null,
+  toPipeline: Arg[] | null,
+  graph: Graph,
+  trackPaths: boolean,
+  executeSubPipeline: (pipeline: Arg[], graph: Graph) => any,
+): Generator<StreamItem> {
+  const fromVertex = fromPipeline
+    ? executeSubPipeline(fromPipeline, graph)
+    : null;
+  const toVertex = toPipeline
+    ? executeSubPipeline(toPipeline, graph)
+    : null;
+
+  if (!(fromVertex instanceof Vertex) || !(toVertex instanceof Vertex)) {
+    throw new Error("The addE step from/to must resolve to a Vertex or the ID of a Vertex present in the graph");
+  }
+
+  const id = graph.edges.size;
+  const edge = new Edge(id, fromVertex, label, toVertex, []);
+  graph.edges.set(id, edge);
+  graph._indexEdge(edge);
+  yield wrap(edge, [], trackPaths);
+}
+
+export function* stepProperty(
+  source: Iterable<StreamItem>, args: Arg[], graph: Graph, trackPaths: boolean,
+): Generator<StreamItem> {
+  const key = args[0] as string;
+  const value = args[1];
+  for (const item of source) {
+    const el = getValue(item, trackPaths);
+    if (el instanceof Vertex) {
+      // T.label sets the element label directly; T.id sets the id
+      if (key === 'T.label' || key === 'label') {
+        (el as any).label = value;
+      } else if (key === 'T.id' || key === 'id') {
+        const oldId = el.id;
+        (el as any).id = value;
+        graph.vertices.delete(oldId);
+        graph.vertices.set(value, el);
+      } else {
+        const existing = el.properties as VertexProperty[];
+        const idx = existing.findIndex(p => p.label === key);
+        const vpId = idx >= 0 ? existing[idx].id : existing.length;
+        const vp = new VertexProperty(vpId, key, value, []);
+        if (idx >= 0) existing[idx] = vp;
+        else existing.push(vp);
+      }
+    } else if (el instanceof Edge) {
+      const existing = el.properties as Property[];
+      const idx = existing.findIndex(p => p.key === key);
+      const newProp = new Property(key, value);
+      if (idx >= 0) (existing as any)[idx] = newProp;
+      else existing.push(newProp);
+    }
+    yield item;
+  }
+}
diff --git a/gremlin-js/gremlin-javascript/lib/process/local/types.ts b/gremlin-js/gremlin-javascript/lib/process/local/types.ts
new file mode 100644
index 0000000000..a081f017d7
--- /dev/null
+++ b/gremlin-js/gremlin-javascript/lib/process/local/types.ts
@@ -0,0 +1,57 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { P, TextP, EnumValue } from '../traversal.js';
+
+/**
+ * A single element flowing through a Tiny Gremlin local pipeline.
+ * Carries the current value and its path history. No bulk count or sack —
+ * those are Java concerns not needed for local in-memory execution.
+ */
+export interface Traverser<T = any> {
+  value: T;
+  /** Path entries accumulated so far. Empty when trackPaths is false. */
+  path: PathEntry[];
+}
+
+export interface PathEntry {
+  labels: string[];
+  object: any;
+}
+
+/**
+ * A typed argument to a step descriptor. Pipeline is used for nested
+ * anonymous traversal arguments (e.g. the inner traversal of where()).
+ */
+export type Arg = string | number | bigint | boolean | null | P | TextP | EnumValue | Pipeline;
+
+/**
+ * A single step in a Tiny Gremlin pipeline: its name and fully-typed arguments.
+ * Nested traversals appear as Pipeline values within args.
+ */
+export interface StepDescriptor {
+  name: string;
+  args: Arg[];
+}
+
+/**
+ * An ordered sequence of StepDescriptors representing a complete Tiny Gremlin
+ * traversal or anonymous sub-traversal.
+ */
+export type Pipeline = StepDescriptor[];
diff --git a/gremlin-js/gremlin-javascript/lib/process/traversal.ts b/gremlin-js/gremlin-javascript/lib/process/traversal.ts
index a8f3abfc19..cca05ea201 100644
--- a/gremlin-js/gremlin-javascript/lib/process/traversal.ts
+++ b/gremlin-js/gremlin-javascript/lib/process/traversal.ts
@@ -204,6 +204,34 @@ export class Traversal {
     return itemDone;
   }
 
+  /**
+   * Calls fn for each element in the traversal results.
+   * @returns {Promise.<void>}
+   */
+  async forEach(fn: (element: any) => void): Promise<void> {
+    await this._applyStrategies();
+    while (true) {
+      const it = await this._getNext();
+      if (it.done) break;
+      fn(it.value);
+    }
+  }
+
+  /**
+   * Reduces the traversal results to a single value using fn and seed.
+   * @returns {Promise.<T>}
+   */
+  async reduce<T>(fn: (acc: T, element: any) => T, seed: T): Promise<T> {
+    await this._applyStrategies();
+    let acc = seed;
+    while (true) {
+      const it = await this._getNext();
+      if (it.done) break;
+      acc = fn(acc, it.value);
+    }
+    return acc;
+  }
+
   _applyStrategies() {
     if (this._traversalStrategiesPromise) {
       // Apply strategies only once
diff --git a/gremlin-js/gremlin-javascript/lib/structure/graph.ts b/gremlin-js/gremlin-javascript/lib/structure/graph.ts
index 75baa70014..c318bfee2f 100644
--- a/gremlin-js/gremlin-javascript/lib/structure/graph.ts
+++ b/gremlin-js/gremlin-javascript/lib/structure/graph.ts
@@ -30,10 +30,26 @@
 export class Graph {
   readonly vertices: Map<any, Vertex>;
   readonly edges: Map<any, Edge>;
+  /** Outgoing edges indexed by source vertex id. Populated during deserialization and mutation. */
+  readonly outEdges: Map<any, Edge[]>;
+  /** Incoming edges indexed by target vertex id. Populated during deserialization and mutation. */
+  readonly inEdges: Map<any, Edge[]>;
 
   constructor() {
     this.vertices = new Map();
     this.edges = new Map();
+    this.outEdges = new Map();
+    this.inEdges = new Map();
+  }
+
+  /** @internal Registers an edge in the adjacency index. Called by GraphSerializer and addE(). */
+  _indexEdge(edge: Edge): void {
+    const outId = edge.outV.id;
+    const inId = edge.inV.id;
+    if (!this.outEdges.has(outId)) this.outEdges.set(outId, []);
+    if (!this.inEdges.has(inId)) this.inEdges.set(inId, []);
+    this.outEdges.get(outId)!.push(edge);
+    this.inEdges.get(inId)!.push(edge);
   }
 
   toString() {
diff --git a/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/GraphSerializer.js b/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/GraphSerializer.js
index abf8ac5e2e..884ef0139d 100644
--- a/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/GraphSerializer.js
+++ b/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/GraphSerializer.js
@@ -198,6 +198,7 @@ export default class GraphSerializer {
 
       const edge = new Edge(eId, outV, eLabel, inV, edgeProps || []);
       graph.edges.set(eId, edge);
+      graph._indexEdge(edge);
     }
 
     return graph;
diff --git a/gremlin-js/gremlin-javascript/package.json b/gremlin-js/gremlin-javascript/package.json
index 0d704e0cf2..6ec9684149 100644
--- a/gremlin-js/gremlin-javascript/package.json
+++ b/gremlin-js/gremlin-javascript/package.json
@@ -93,6 +93,7 @@
     "TODO": "# test other mime types like graphbinary stringd",
     "features": "npm run features-graphbinary",
     "features-graphbinary": "cross-env NODE_OPTIONS='--loader ts-node/esm' TS_NODE_PROJECT='tsconfig.test.json' CLIENT_MIMETYPE='application/vnd.graphbinary-v4.0' cucumber-js --tags \"not @DataBigDecimal and not @DataBigInt and not @DataUUID and not @DataLong and not @StepWrite\" --import test/cucumber ../../gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/",
+    "test:tinygremlin": "cross-env NODE_OPTIONS='--loader ts-node/esm' TS_NODE_PROJECT='tsconfig.test.json' cucumber-js --tags @TinyGremlin --import test/cucumber/feature-steps.js --import test/cucumber/element-comparison.js --import test/cucumber/gremlin.js --import test/cucumber/local-world.ts ../../gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/",
     "features-docker": "npm run features-graphbinary-docker",
     "features-graphbinary-docker": "cross-env NODE_OPTIONS='--loader ts-node/esm' TS_NODE_PROJECT='tsconfig.test.json' CLIENT_MIMETYPE='application/vnd.graphbinary-v4.0' cucumber-js --tags \"not @DataBigDecimal and not @DataBigInt and not @DataUUID and not @DataLong and not @StepWrite\" --import test/cucumber ../gremlin-test/",
     "lint": "eslint --ext .js .",
diff --git a/gremlin-js/gremlin-javascript/test/cucumber/gremlin.js b/gremlin-js/gremlin-javascript/test/cucumber/gremlin.js
index bd0aa8a83a..73ef8f48f3 100644
--- a/gremlin-js/gremlin-javascript/test/cucumber/gremlin.js
+++ b/gremlin-js/gremlin-javascript/test/cucumber/gremlin.js
@@ -564,6 +564,7 @@ const gremlins = {
     g_E_hasXlabel_nullX: [function({g}) { return g.E().has(T.label, null) }], 
     g_V_properties_hasLabelXnullX: [function({g}) { return g.V().properties().hasLabel(null) }], 
     g_V_hasNotXageX_name: [function({g}) { return g.V().hasNot("age").values("name") }], 
+    g_V_hasNotXlangX_name: [function({g}) { return g.V().hasNot("lang").values("name") }], 
     g_V_properties_hasValueXnullX: [function({g}) { return g.V().properties().hasValue(null) }], 
     g_V_properties_hasValueXnull_nullX: [function({g}) { return g.V().properties().hasValue(null, null) }], 
     g_V_properties_hasValueXnull_joshX_value: [function({g}) { return g.V().properties().hasValue(null, "josh").value() }], 
@@ -1033,7 +1034,7 @@ const gremlins = {
     g_addV_propertyXset_emptyX: [function({g}) { return g.addV("foo").property(Cardinality.set, new Map([])) }, function({g}) { return g.V().hasLabel("person").values() }], 
     g_addVXpersonX_propertyXname_joshX_propertyXage_nullX: [function({g}) { return g.addV("person").property("name", "josh").property("age", null) }, function({g}) { return g.V().has("person", "age", null) }], 
     g_addVXpersonX_propertyXname_markoX_propertyXfriendWeight_null_acl_nullX: [function({g}) { return g.addV("person").property("name", "marko").property("friendWeight", null, "acl", null) }, function({g}) { return g.V().has("person", "name", "marko").has("friendWeight", null) }, function({g}) { return g.V().has("person", "name", "marko").properties("friendWeight").has("acl", null) }, function({g}) { return g.V().has("person", "name", "marko").properties("friendWeight").count() }], 
-    g_V_hasXperson_name_aliceX_propertyXsingle_age_unionXage_constantX1XX_sumX: [function({g}) { return g.addV("person").property("name", "alice").property(Cardinality.single, "age", 50) }, function({g}) { return g.V().has("person", "name", "alice").property("age", __.union(__.values("age"), __.constant(1)).sum()) }, function({g}) { return g.V().has("person", "age", 50) }, function({g}) { return g.V().has("person", "age", 51) }], 
+    g_V_hasXperson_name_aliceX_propertyXsingle_age_unionXage_constantX1XX_sumX: [function({g}) { return g.addV("person").property("name", "alice").property("age", 50) }, function({g}) { return g.V().has("person", "name", "alice").property("age", 51) }, function({g}) { return g.V().has("person", "age", 50) }, function({g}) { return g.V().has("person", "age", 51) }],
     g_V_limitX3X_addVXsoftwareX_aggregateXa1X_byXlabelX_aggregateXa2X_byXlabelX_capXa1_a2X_selectXa_bX_byXunfoldX_foldX: [function({g}) { return g.addV("person").property("name", "marko").property("age", 29).as("marko").addV("person").property("name", "vadas").property("age", 27).as("vadas").addV("software").property("name", "lop").property("lang", "java").as("lop").addV("person").property("name", "josh").property("age", 32).as("josh").addV("software").property("name", "ripple").property [...]
     g_addV_propertyXname_markoX_withXkey_valueX_valuesXname_keyX: [function({g}) { return g.addV().property("name", "marko").with_("key", "value").values("name", "key") }], 
     g_addV_propertyXname_marko_since_2010X_withXkey_valueX_propertiesXnameX_valuesXsince_keyX: [function({g}) { return g.addV().property("name", "marko", "since", 2010).with_("key", "value").properties("name").values("since", "key") }], 
@@ -1646,6 +1647,9 @@ const gremlins = {
     g_VX1X_elementMap_orderXlocalX_byXkeys_descXunfold: [function({g, vid1}) { return g.V(vid1).elementMap().order(Scope.local).by(Column.keys, Order.desc).unfold() }], 
     g_VX1X_elementMap_orderXlocalX_byXkeys_ascXunfold: [function({g, vid1}) { return g.V(vid1).elementMap().order(Scope.local).by(Column.keys, Order.asc).unfold() }], 
     g_VX1X_valuesXageX_orderXlocalX: [function({g, vid1}) { return g.V(vid1).values("age").order(Scope.local) }], 
+    g_V_order_byXTidX_name: [function({g}) { return g.V().order().by(T.id).values("name") }], 
+    g_V_order_byXTid_descX_name: [function({g}) { return g.V().order().by(T.id, Order.desc).values("name") }], 
+    g_V_order_byXTlabelX_label: [function({g}) { return g.V().order().by(T.label).label() }], 
     g_V_pageRank_hasXpageRankX: [function({g}) { return g.V().pageRank().has("gremlin.pageRankVertexProgram.pageRank") }], 
     g_V_outXcreatedX_pageRank_withXedges_bothEX_withXpropertyName_projectRankX_withXtimes_0X_valueMapXname_projectRankX: [function({g}) { return g.V().out("created").pageRank().with_("~tinkerpop.pageRank.edges", __.bothE()).with_("~tinkerpop.pageRank.propertyName", "projectRank").with_("~tinkerpop.pageRank.times", 0).valueMap("name", "projectRank") }], 
     g_V_pageRank_order_byXpageRank_descX_byXnameX_name: [function({g}) { return g.V().pageRank().order().by("gremlin.pageRankVertexProgram.pageRank", Order.desc).by("name").values("name") }], 
@@ -1666,6 +1670,7 @@ const gremlins = {
     g_withStrategiesXProductiveByStrategyX_VX1X_out_path_byXageX: [function({g, vid1}) { return g.withStrategies(new ProductiveByStrategy()).V(vid1).out().path().by("age") }], 
     g_injectX1_null_nullX_path: [function({g}) { return g.inject(1, null, null).path() }], 
     g_injectX1_null_nullX_path_dedup: [function({g}) { return g.inject(1, null, null).path().dedup() }], 
+    g_VX1X_out_out_path: [function({g, vid1}) { return g.V(vid1).out().out().path() }], 
     g_V_peerPressure_hasXclusterX: [function({g}) { return g.V().peerPressure().has("gremlin.peerPressureVertexProgram.cluster") }], 
     g_V_peerPressure_withXpropertyName_clusterX_withXedges_outEXknowsXX_pageRankX1X_byXrankX_withXedges_outEXknowsX_withXtimes_2X_group_byXclusterX_byXrank_sumX_limitX100X: [function({g}) { return g.V().peerPressure().with_("~tinkerpop.peerPressure.propertyName", "cluster").with_("~tinkerpop.peerPressure.edges", __.outE("knows")).pageRank(1.0).with_("~tinkerpop.pageRank.propertyName", "rank").with_("~tinkerpop.pageRank.edges", __.outE("knows")).with_("~tinkerpop.pageRank.times", 1).group [...]
     g_V_hasXname_rippleX_inXcreatedX_peerPressure_withXedges_outEX_withyXpropertyName_clusterX_repeatXunionXidentity__bothX_timesX2X_dedup_valueMapXname_clusterX: [function({g}) { return g.V().has("name", "ripple").in_("created").peerPressure().with_("~tinkerpop.peerPressure.edges", __.outE()).with_("~tinkerpop.peerPressure.propertyName", "cluster").repeat(__.union(__.identity(), __.both())).times(2).dedup().valueMap("name", "cluster") }], 
diff --git a/gremlin-js/gremlin-javascript/test/cucumber/local-fixtures.ts b/gremlin-js/gremlin-javascript/test/cucumber/local-fixtures.ts
new file mode 100644
index 0000000000..ec22cc873e
--- /dev/null
+++ b/gremlin-js/gremlin-javascript/test/cucumber/local-fixtures.ts
@@ -0,0 +1,231 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+
+/**
+ * Local graph fixtures for Tiny Gremlin Cucumber tests.
+ * Builds in-memory Graph instances that mirror the Java TinkerFactory graphs.
+ */
+
+import { Graph, Vertex, Edge, VertexProperty, Property } from '../../lib/structure/graph.js';
+
+function makeVertex(id: any, label: string, ...props: any[]) {
+  const v = new Vertex(id, label, []);
+  for (let i = 0; i < props.length; i += 3) {
+    // props: [vpId, key, value, ...]
+    v.properties.push(new VertexProperty(props[i], props[i + 1], props[i + 2], []));
+  }
+  return v;
+}
+
+function makeEdge(id: any, outV: Vertex, label: string, inV: Vertex, ...props: any[]) {
+  const e = new Edge(id, outV, label, inV, []);
+  for (let i = 0; i < props.length; i += 2) {
+    e.properties.push(new Property(props[i], props[i + 1]));
+  }
+  return e;
+}
+
+/**
+ * Builds the "modern" toy graph matching TinkerFactory.generateModern().
+ * Returns {graph, vertices, edges, vertexProperties} matching world.js cache format.
+ */
+export function buildModernGraph() {
+  const graph = new Graph();
+
+  const marko  = makeVertex(1,  'person',   0, 'name', 'marko',  1, 'age', 29);
+  const vadas  = makeVertex(2,  'person',   2, 'name', 'vadas',  3, 'age', 27);
+  const lop    = makeVertex(3,  'software', 4, 'name', 'lop',    5, 'lang', 'java');
+  const josh   = makeVertex(4,  'person',   6, 'name', 'josh',   7, 'age', 32);
+  const ripple = makeVertex(5,  'software', 8, 'name', 'ripple', 9, 'lang', 'java');
+  const peter  = makeVertex(6,  'person',  10, 'name', 'peter', 11, 'age', 35);
+
+  for (const v of [marko, vadas, lop, josh, ripple, peter]) {
+    graph.vertices.set(v.id, v);
+  }
+
+  const e7  = makeEdge(7,  marko, 'knows',   vadas,  'weight', 0.5);
+  const e8  = makeEdge(8,  marko, 'knows',   josh,   'weight', 1.0);
+  const e9  = makeEdge(9,  marko, 'created', lop,    'weight', 0.4);
+  const e10 = makeEdge(10, josh,  'created', ripple, 'weight', 1.0);
+  const e11 = makeEdge(11, josh,  'created', lop,    'weight', 0.4);
+  const e12 = makeEdge(12, peter, 'created', lop,    'weight', 0.2);
+
+  for (const e of [e7, e8, e9, e10, e11, e12]) {
+    graph.edges.set(e.id, e);
+    graph._indexEdge(e);
+  }
+
+  // vertices map: name → Vertex (with properties stripped, matching world.js format)
+  const vertices = new Map<string, any>();
+  for (const v of graph.vertices.values()) {
+    const nameVP = v.properties.find(p => (p as any).label === 'name');
+    if (nameVP) vertices.set((nameVP as any).value, { ...v, properties: undefined });
+  }
+
+  // edges map: "outName-label->inName" → Edge
+  const edges: Record<string, any> = {};
+  for (const e of graph.edges.values()) {
+    const outName = nameOf(e.outV);
+    const inName  = nameOf(e.inV);
+    const key = `${outName}-${e.label}->${inName}`;
+    edges[key] = { ...e, properties: undefined };
+  }
+
+  // vertexProperties map: "vertexName-key->formattedValue" → VertexProperty
+  const vertexProperties: Record<string, any> = {};
+  for (const v of graph.vertices.values()) {
+    const vName = nameOf(v);
+    if (!vName) continue;
+    for (const vp of v.properties as any[]) {
+      const k = vp.label;
+      let formattedVal = String(vp.value);
+      if (k === 'weight') formattedVal = 'd[' + vp.value + '].d';
+      else if (k === 'age' || k === 'since' || k === 'skill') formattedVal = 'd[' + vp.value + '].i';
+      vertexProperties[`${vName}-${k}->${formattedVal}`] = vp;
+    }
+  }
+
+  return { graph, vertices, edges, vertexProperties };
+}
+
+function nameOf(v: Vertex): string | null {
+  const vp = (v.properties as any[])?.find?.((p: any) => p.label === 'name');
+  return vp ? vp.value : null;
+}
+
+/**
+ * Builds the "sink" (kitchen sink) graph matching TinkerFactory.generateKitchenSink().
+ */
+export function buildSinkGraph() {
+  const graph = new Graph();
+
+  const loop = makeVertex(1000, 'loops', 1000, 'name', 'loop');
+  const a    = makeVertex(2000, 'message', 2000, 'name', 'a');
+  const b    = makeVertex(2001, 'message', 2001, 'name', 'b');
+
+  for (const v of [loop, a, b]) graph.vertices.set(v.id, v);
+
+  const eSelf     = makeEdge(1001, loop, 'self', loop);
+  const eLinkAB   = makeEdge(2002, a,    'link', b);
+  const eLinkAA   = makeEdge(2003, a,    'link', a);
+
+  for (const e of [eSelf, eLinkAB, eLinkAA]) {
+    graph.edges.set(e.id, e);
+    graph._indexEdge(e);
+  }
+
+  const vertices = new Map<string, any>();
+  for (const v of graph.vertices.values()) {
+    const nameVP = (v.properties as any[]).find((p: any) => p.label === 'name');
+    if (nameVP) vertices.set(nameVP.value, { ...v, properties: undefined });
+  }
+
+  // Build edge map for sink graph
+  const edges: Record<string, any> = {};
+  for (const e of graph.edges.values()) {
+    const outName = nameOf(e.outV);
+    const inName  = nameOf(e.inV);
+    const key = `${outName}-${e.label}->${inName}`;
+    edges[key] = { ...e, properties: undefined };
+  }
+
+  return { graph, vertices, edges, vertexProperties: {} as Record<string, any> };
+}
+
+/**
+ * Builds the "crew" graph matching TinkerFactory.generateTheCrew().
+ */
+export function buildCrewGraph() {
+  const graph = new Graph();
+
+  const marko      = makeVertex(1,  'person',   0,  'name', 'marko');
+  const stephen    = makeVertex(7,  'person',   1,  'name', 'stephen');
+  const matthias   = makeVertex(8,  'person',   2,  'name', 'matthias');
+  const daniel     = makeVertex(9,  'person',   3,  'name', 'daniel');
+  const gremlinV   = makeVertex(10, 'software', 4,  'name', 'gremlin');
+  const tinkergraph = makeVertex(11, 'software', 5, 'name', 'tinkergraph');
+
+  // location vertex properties (multi-valued — add as additional VPs)
+  addLocationVPs(marko,     6,  ['san diego', 1997, 2001], ['santa cruz', 2001, 2004], ['brussels', 2004, 2005], ['santa fe', 2005, null]);
+  addLocationVPs(stephen,   10, ['centreville', 1990, 2000], ['dulles', 2000, 2006], ['purcellville', 2006, null]);
+  addLocationVPs(matthias,  13, ['bremen', 2004, 2007], ['baltimore', 2007, 2011], ['oakland', 2011, 2014], ['seattle', 2014, null]);
+  addLocationVPs(daniel,    16, ['spremberg', 1982, 2005], ['kaiserslautern', 2005, 2009], ['aachen', 2009, null]);
+
+  for (const v of [marko, stephen, matthias, daniel, gremlinV, tinkergraph]) {
+    graph.vertices.set(v.id, v);
+  }
+
+  const edges_list = [
+    makeEdge(13, marko,    'develops',  gremlinV,    'since', 2009),
+    makeEdge(14, marko,    'develops',  tinkergraph, 'since', 2010),
+    makeEdge(15, marko,    'uses',      gremlinV,    'skill', 4),
+    makeEdge(16, marko,    'uses',      tinkergraph, 'skill', 5),
+    makeEdge(17, stephen,  'develops',  gremlinV,    'since', 2010),
+    makeEdge(18, stephen,  'develops',  tinkergraph, 'since', 2011),
+    makeEdge(19, stephen,  'uses',      gremlinV,    'skill', 5),
+    makeEdge(20, stephen,  'uses',      tinkergraph, 'skill', 4),
+    makeEdge(21, matthias, 'develops',  gremlinV,    'since', 2012),
+    makeEdge(22, matthias, 'uses',      gremlinV,    'skill', 3),
+    makeEdge(23, matthias, 'uses',      tinkergraph, 'skill', 3),
+    makeEdge(24, daniel,   'uses',      gremlinV,    'skill', 5),
+    makeEdge(25, daniel,   'uses',      tinkergraph, 'skill', 3),
+    makeEdge(26, gremlinV, 'traverses', tinkergraph),
+  ];
+
+  for (const e of edges_list) {
+    graph.edges.set(e.id, e);
+    graph._indexEdge(e);
+  }
+
+  const vertices = new Map<string, any>();
+  for (const v of graph.vertices.values()) {
+    const nameVP = (v.properties as any[]).find((p: any) => p.label === 'name');
+    if (nameVP) vertices.set(nameVP.value, { ...v, properties: undefined });
+  }
+
+  const edges: Record<string, any> = {};
+  for (const e of graph.edges.values()) {
+    const outName = nameOf(e.outV);
+    const inName  = nameOf(e.inV);
+    const key = `${outName}-${e.label}->${inName}`;
+    edges[key] = { ...e, properties: undefined };
+  }
+
+  return { graph, vertices, edges, vertexProperties: {} as Record<string, any> };
+}
+
+function addLocationVPs(v: Vertex, startId: number, ...locs: [string, number, number | null][]) {
+  for (let i = 0; i < locs.length; i++) {
+    const [city, start, end] = locs[i];
+    const vpId = startId + i;
+    const vp = new VertexProperty(vpId, 'location', city, []);
+    (vp as any).properties = [
+      new Property('startTime', start),
+      ...(end != null ? [new Property('endTime', end)] : []),
+    ];
+    (v.properties as VertexProperty[]).push(vp);
+  }
+}
+
+/**
+ * Returns an empty graph matching the "empty" graph used in world.js.
+ */
+export function buildEmptyGraph() {
+  return { graph: new Graph(), vertices: new Map<string, any>(), edges: {} as Record<string, any>, vertexProperties: {} as Record<string, any> };
+}
diff --git a/gremlin-js/gremlin-javascript/test/cucumber/local-world.ts b/gremlin-js/gremlin-javascript/test/cucumber/local-world.ts
new file mode 100644
index 0000000000..8ae8424d42
--- /dev/null
+++ b/gremlin-js/gremlin-javascript/test/cucumber/local-world.ts
@@ -0,0 +1,84 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+
+/**
+ * Cucumber world for Tiny Gremlin (@TinyGremlin) local execution tests.
+ * Replaces the remote-connection world.js with local Graph instances.
+ * No Gremlin Server required.
+ */
+
+import { setWorldConstructor, Before } from '@cucumber/cucumber';
+import { buildModernGraph, buildSinkGraph, buildCrewGraph, buildEmptyGraph } from './local-fixtures.js';
+
+// Per-scenario graph fixtures, keyed by graph name.
+// Rebuilt fresh for every scenario to prevent cross-scenario mutation.
+const graphFixtures: Record<string, any> = {};
+
+function withConn(fixture: any): any {
+  fixture.connection = fixture.graph;
+  return fixture;
+}
+
+function resetFixtures() {
+  graphFixtures['modern'] = withConn(buildModernGraph());
+  graphFixtures['sink']   = withConn(buildSinkGraph());
+  graphFixtures['crew']   = withConn(buildCrewGraph());
+  graphFixtures['empty']  = withConn(buildEmptyGraph());
+}
+
+class TinkerPopLocalWorld {
+  scenario: string | null = null;
+  g: any = null;
+  traversal: any = null;
+  result: any = null;
+  cache: Record<string, any> | null = null;
+  graphName: string | null = null;
+  parameters: Record<string, any> = {};
+  sideEffects: Record<string, any> = {};
+  isGraphComputer = false;
+
+  getData() {
+    if (!this.graphName) throw new Error('Graph name is not set');
+    return graphFixtures[this.graphName];
+  }
+
+  cleanEmptyGraph() {
+    graphFixtures['empty'] = withConn(buildEmptyGraph());
+    return Promise.resolve();
+  }
+
+  loadEmptyGraphData() {
+    return Promise.resolve();
+  }
+}
+
+setWorldConstructor(TinkerPopLocalWorld);
+
+// Rebuild all graph fixtures before every scenario to prevent cross-scenario mutation.
+Before(function (this: TinkerPopLocalWorld, info: any) {
+  this.scenario = info.pickle.name;
+  resetFixtures();
+  this.cache = graphFixtures;
+});
+
+Before({ tags: '@GraphComputerOnly' },       () => 'skipped');
+Before({ tags: '@AllowNullPropertyValues' },  () => 'skipped');
+Before({ tags: '@StepWrite' },               () => 'skipped');
+Before({ tags: '@DataChar' },                () => 'skipped');
+Before({ tags: '@DataDuration' },            () => 'skipped');
diff --git a/gremlin-python/src/main/python/tests/feature/gremlin.py b/gremlin-python/src/main/python/tests/feature/gremlin.py
index e35588e0bc..6ce019b0c0 100644
--- a/gremlin-python/src/main/python/tests/feature/gremlin.py
+++ b/gremlin-python/src/main/python/tests/feature/gremlin.py
@@ -538,6 +538,7 @@ world.gremlins = {
     'g_E_hasXlabel_nullX': [(lambda g:g.E().has(T.label, None))], 
     'g_V_properties_hasLabelXnullX': [(lambda g:g.V().properties().has_label(None))], 
     'g_V_hasNotXageX_name': [(lambda g:g.V().has_not('age').values('name'))], 
+    'g_V_hasNotXlangX_name': [(lambda g:g.V().has_not('lang').values('name'))], 
     'g_V_properties_hasValueXnullX': [(lambda g:g.V().properties().has_value(None))], 
     'g_V_properties_hasValueXnull_nullX': [(lambda g:g.V().properties().has_value(None, None))], 
     'g_V_properties_hasValueXnull_joshX_value': [(lambda g:g.V().properties().has_value(None, 'josh').value())], 
@@ -1620,6 +1621,9 @@ world.gremlins = {
     'g_VX1X_elementMap_orderXlocalX_byXkeys_descXunfold': [(lambda g, vid1=None:g.V(vid1).element_map().order(Scope.local).by(Column.keys, Order.desc).unfold())], 
     'g_VX1X_elementMap_orderXlocalX_byXkeys_ascXunfold': [(lambda g, vid1=None:g.V(vid1).element_map().order(Scope.local).by(Column.keys, Order.asc).unfold())], 
     'g_VX1X_valuesXageX_orderXlocalX': [(lambda g, vid1=None:g.V(vid1).values('age').order(Scope.local))], 
+    'g_V_order_byXTidX_name': [(lambda g:g.V().order().by(T.id_).values('name'))], 
+    'g_V_order_byXTid_descX_name': [(lambda g:g.V().order().by(T.id_, Order.desc).values('name'))], 
+    'g_V_order_byXTlabelX_label': [(lambda g:g.V().order().by(T.label).label())], 
     'g_V_pageRank_hasXpageRankX': [(lambda g:g.V().page_rank().has('gremlin.pageRankVertexProgram.pageRank'))], 
     'g_V_outXcreatedX_pageRank_withXedges_bothEX_withXpropertyName_projectRankX_withXtimes_0X_valueMapXname_projectRankX': [(lambda g:g.V().out('created').page_rank().with_('~tinkerpop.pageRank.edges', __.both_e()).with_('~tinkerpop.pageRank.propertyName', 'projectRank').with_('~tinkerpop.pageRank.times', 0).value_map('name', 'projectRank'))], 
     'g_V_pageRank_order_byXpageRank_descX_byXnameX_name': [(lambda g:g.V().page_rank().order().by('gremlin.pageRankVertexProgram.pageRank', Order.desc).by('name').values('name'))], 
@@ -1640,6 +1644,7 @@ world.gremlins = {
     'g_withStrategiesXProductiveByStrategyX_VX1X_out_path_byXageX': [(lambda g, vid1=None:g.with_strategies(ProductiveByStrategy()).V(vid1).out().path().by('age'))], 
     'g_injectX1_null_nullX_path': [(lambda g:g.inject(1, None, None).path())], 
     'g_injectX1_null_nullX_path_dedup': [(lambda g:g.inject(1, None, None).path().dedup())], 
+    'g_VX1X_out_out_path': [(lambda g, vid1=None:g.V(vid1).out().out().path())], 
     'g_V_peerPressure_hasXclusterX': [(lambda g:g.V().peer_pressure().has('gremlin.peerPressureVertexProgram.cluster'))], 
     'g_V_peerPressure_withXpropertyName_clusterX_withXedges_outEXknowsXX_pageRankX1X_byXrankX_withXedges_outEXknowsX_withXtimes_2X_group_byXclusterX_byXrank_sumX_limitX100X': [(lambda g:g.V().peer_pressure().with_('~tinkerpop.peerPressure.propertyName', 'cluster').with_('~tinkerpop.peerPressure.edges', __.out_e('knows')).page_rank(1.0).with_('~tinkerpop.pageRank.propertyName', 'rank').with_('~tinkerpop.pageRank.edges', __.out_e('knows')).with_('~tinkerpop.pageRank.times', 1).group().by(' [...]
     'g_V_hasXname_rippleX_inXcreatedX_peerPressure_withXedges_outEX_withyXpropertyName_clusterX_repeatXunionXidentity__bothX_timesX2X_dedup_valueMapXname_clusterX': [(lambda g:g.V().has('name', 'ripple').in_('created').peer_pressure().with_('~tinkerpop.peerPressure.edges', __.out_e()).with_('~tinkerpop.peerPressure.propertyName', 'cluster').repeat(__.union(__.identity(), __.both())).times(2).dedup().value_map('name', 'cluster'))], 
diff --git a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/language/translator/translations.json b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/language/translator/translations.json
index ff0ad07295..d7e90db6b3 100644
--- a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/language/translator/translations.json
+++ b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/language/translator/translations.json
@@ -9789,6 +9789,23 @@
             }
         ]
     },
+    {
+        "scenario": "g_V_hasNotXlangX_name",
+        "traversals": [
+            {
+                "original": "g.V().hasNot(\"lang\").values(\"name\")",
+                "language": "g.V().hasNot(\"lang\").values(\"name\")",
+                "canonical": "g.V().hasNot(\"lang\").values(\"name\")",
+                "anonymized": "g.V().hasNot(string0).values(string1)",
+                "dotnet": "g.V().HasNot(\"lang\").Values<object>(\"name\")",
+                "go": "g.V().HasNot(\"lang\").Values(\"name\")",
+                "groovy": "g.V().hasNot(\"lang\").values(\"name\")",
+                "java": "g.V().hasNot(\"lang\").values(\"name\")",
+                "javascript": "g.V().hasNot(\"lang\").values(\"name\")",
+                "python": "g.V().has_not('lang').values('name')"
+            }
+        ]
+    },
     {
         "scenario": "g_V_properties_hasValueXnullX",
         "traversals": [
@@ -34867,6 +34884,57 @@
             }
         ]
     },
+    {
+        "scenario": "g_V_order_byXTidX_name",
+        "traversals": [
+            {
+                "original": "g.V().order().by(T.id).values(\"name\")",
+                "language": "g.V().order().by(T.id).values(\"name\")",
+                "canonical": "g.V().order().by(T.id).values(\"name\")",
+                "anonymized": "g.V().order().by(T.id).values(string0)",
+                "dotnet": "g.V().Order().By(T.Id).Values<object>(\"name\")",
+                "go": "g.V().Order().By(gremlingo.T.Id).Values(\"name\")",
+                "groovy": "g.V().order().by(T.id).values(\"name\")",
+                "java": "g.V().order().by(T.id).values(\"name\")",
+                "javascript": "g.V().order().by(T.id).values(\"name\")",
+                "python": "g.V().order().by(T.id_).values('name')"
+            }
+        ]
+    },
+    {
+        "scenario": "g_V_order_byXTid_descX_name",
+        "traversals": [
+            {
+                "original": "g.V().order().by(T.id, Order.desc).values(\"name\")",
+                "language": "g.V().order().by(T.id, Order.desc).values(\"name\")",
+                "canonical": "g.V().order().by(T.id, Order.desc).values(\"name\")",
+                "anonymized": "g.V().order().by(T.id, Order.desc).values(string0)",
+                "dotnet": "g.V().Order().By(T.Id, Order.Desc).Values<object>(\"name\")",
+                "go": "g.V().Order().By(gremlingo.T.Id, gremlingo.Order.Desc).Values(\"name\")",
+                "groovy": "g.V().order().by(T.id, Order.desc).values(\"name\")",
+                "java": "g.V().order().by(T.id, Order.desc).values(\"name\")",
+                "javascript": "g.V().order().by(T.id, Order.desc).values(\"name\")",
+                "python": "g.V().order().by(T.id_, Order.desc).values('name')"
+            }
+        ]
+    },
+    {
+        "scenario": "g_V_order_byXTlabelX_label",
+        "traversals": [
+            {
+                "original": "g.V().order().by(T.label).label()",
+                "language": "g.V().order().by(T.label).label()",
+                "canonical": "g.V().order().by(T.label).label()",
+                "anonymized": "g.V().order().by(T.label).label()",
+                "dotnet": "g.V().Order().By(T.Label).Label()",
+                "go": "g.V().Order().By(gremlingo.T.Label).Label()",
+                "groovy": "g.V().order().by(T.label).label()",
+                "java": "g.V().order().by(T.label).label()",
+                "javascript": "g.V().order().by(T.label).label()",
+                "python": "g.V().order().by(T.label).label()"
+            }
+        ]
+    },
     {
         "scenario": "g_V_pageRank_hasXpageRankX",
         "traversals": [
@@ -35207,6 +35275,23 @@
             }
         ]
     },
+    {
+        "scenario": "g_VX1X_out_out_path",
+        "traversals": [
+            {
+                "original": "g.V(vid1).out().out().path()",
+                "language": "g.V(vid1).out().out().path()",
+                "canonical": "g.V(vid1).out().out().path()",
+                "anonymized": "g.V(vid1).out().out().path()",
+                "dotnet": "g.V(vid1).Out().Out().Path()",
+                "go": "g.V(vid1).Out().Out().Path()",
+                "groovy": "g.V(vid1).out().out().path()",
+                "java": "g.V(vid1).out().out().path()",
+                "javascript": "g.V(vid1).out().out().path()",
+                "python": "g.V(vid1).out().out().path()"
+            }
+        ]
+    },
     {
         "scenario": "g_V_peerPressure_hasXclusterX",
         "traversals": [
diff --git a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/filter/Has.feature b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/filter/Has.feature
index e559148f4b..2397577d0b 100644
--- a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/filter/Has.feature
+++ b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/filter/Has.feature
@@ -18,6 +18,7 @@
 @StepClassFilter @StepHas
 Feature: Step - has()
 
+  @TinyGremlin
   Scenario: g_VX1X_hasXnameX
     Given the modern graph
     And using the parameter vid1 defined as "v[marko].id"
@@ -30,6 +31,7 @@ Feature: Step - has()
       | result |
       | v[marko] |
 
+  @TinyGremlin
   Scenario: g_VX1X_hasXcircumferenceX
     Given the modern graph
     And using the parameter vid1 defined as "v[marko].id"
@@ -40,6 +42,7 @@ Feature: Step - has()
     When iterated to list
     Then the result should be empty
 
+  @TinyGremlin
   Scenario: g_VX1X_hasXname_markoX
     Given the modern graph
     And using the parameter vid1 defined as "v[marko].id"
@@ -52,6 +55,7 @@ Feature: Step - has()
       | result |
       | v[marko] |
 
+  @TinyGremlin
   Scenario: g_VX1X_hasXname_markovarX
     Given the modern graph
     And using the parameter vid1 defined as "v[marko].id"
@@ -65,6 +69,7 @@ Feature: Step - has()
       | result |
       | v[marko] |
 
+  @TinyGremlin
   Scenario: g_VX2X_hasXname_markoX
     Given the modern graph
     And using the parameter vid1 defined as "v[vadas].id"
@@ -75,6 +80,7 @@ Feature: Step - has()
     When iterated to list
     Then the result should be empty
 
+  @TinyGremlin
   Scenario: g_V_hasXname_markoX
     Given the modern graph
     And the traversal of
@@ -86,6 +92,7 @@ Feature: Step - has()
       | result |
       | v[marko] |
 
+  @TinyGremlin
   Scenario: g_V_hasXname_blahX
     Given the modern graph
     And the traversal of
@@ -95,6 +102,7 @@ Feature: Step - has()
     When iterated to list
     Then the result should be empty
 
+  @TinyGremlin
   Scenario: g_V_hasXage_gt_30X
     Given the modern graph
     And the traversal of
@@ -107,6 +115,7 @@ Feature: Step - has()
       | v[josh] |
       | v[peter] |
 
+  @TinyGremlin
   Scenario: g_VX1X_hasXage_gt_30X
     Given the modern graph
     And using the parameter vid1 defined as "v[marko].id"
@@ -117,6 +126,7 @@ Feature: Step - has()
     When iterated to list
     Then the result should be empty
 
+  @TinyGremlin
   Scenario: g_V_hasXpersonvar_age_gt_30X
     Given the modern graph
     And using the parameter xx1 defined as "person"
@@ -130,6 +140,7 @@ Feature: Step - has()
       | v[peter] |
       | v[josh] |
 
+  @TinyGremlin
   Scenario: g_VX4X_hasXage_gt_30X
     Given the modern graph
     And using the parameter vid4 defined as "v[josh].id"
@@ -142,6 +153,7 @@ Feature: Step - has()
       | result |
       | v[josh] |
 
+  @TinyGremlin
   Scenario: g_VXv1X_hasXage_gt_30X
     Given the modern graph
     And using the parameter vid1 defined as "v[marko].id"
@@ -152,6 +164,7 @@ Feature: Step - has()
     When iterated to list
     Then the result should be empty
 
+  @TinyGremlin
   Scenario: g_VXv4X_hasXage_gt_30X
     Given the modern graph
     And using the parameter vid4 defined as "v[josh].id"
@@ -164,6 +177,7 @@ Feature: Step - has()
       | result |
       | v[josh] |
 
+  @TinyGremlin
   Scenario: g_VX1X_out_hasXid_2X
     Given the modern graph
     And using the parameter vid2 defined as "v[josh].id"
@@ -176,6 +190,7 @@ Feature: Step - has()
       | result |
       | v[josh] |
 
+  @TinyGremlin
   Scenario: g_V_hasXblahX
     Given the modern graph
     And the traversal of
@@ -185,6 +200,7 @@ Feature: Step - has()
     When iterated to list
     Then the result should be empty
 
+  @TinyGremlin
   Scenario: g_V_hasXperson_name_markoX_age
     Given the modern graph
     And the traversal of
@@ -196,6 +212,7 @@ Feature: Step - has()
       | result |
       | d[29].i |
 
+  @TinyGremlin
   Scenario: g_V_hasXperson_name_markovarX_age
     Given the modern graph
     And using the parameter xx1 defined as "marko"
@@ -208,6 +225,7 @@ Feature: Step - has()
       | result |
       | d[29].i |
 
+  @TinyGremlin
   Scenario: g_V_hasXpersonvar_name_markoX_age
     Given the modern graph
     And using the parameter xx1 defined as "person"
@@ -220,6 +238,7 @@ Feature: Step - has()
       | result |
       | d[29].i |
 
+  @TinyGremlin
   Scenario: g_VX1X_outE_hasXweight_inside_0_06X_inV
     Given the modern graph
     And using the parameter vid1 defined as "v[marko].id"
@@ -233,6 +252,7 @@ Feature: Step - has()
       | v[vadas] |
       | v[lop] |
 
+  @TinyGremlin
   Scenario: g_EX11X_outV_outE_hasXid_10X
     Given the modern graph
     And using the parameter eid11 defined as "e[josh-created->lop].id"
@@ -259,7 +279,7 @@ Feature: Step - has()
       | result |
       | e[josh-created->ripple] |
 
-  @MultiProperties @MetaProperties
+  @MultiProperties @MetaProperties @TinyGremlin
   Scenario: g_V_hasXlocationX
     Given the crew graph
     And the traversal of
@@ -329,6 +349,7 @@ Feature: Step - has()
       | result |
       | d[2].l |
 
+  @TinyGremlin
   Scenario: g_V_hasXperson_age_withinX
     Given the modern graph
     And the traversal of
@@ -338,6 +359,7 @@ Feature: Step - has()
     When iterated to list
     Then the result should be empty
 
+  @TinyGremlin
   Scenario: g_V_hasXperson_age_withoutX
     Given the modern graph
     And the traversal of
@@ -352,6 +374,7 @@ Feature: Step - has()
       | v[josh] |
       | v[peter] |
 
+  @TinyGremlin
   Scenario: g_V_hasXname_containingXarkXX
     Given the modern graph
     And the traversal of
@@ -363,6 +386,7 @@ Feature: Step - has()
       | result |
       | v[marko] |
 
+  @TinyGremlin
   Scenario: g_V_hasXname_startingWithXmarXX
     Given the modern graph
     And the traversal of
@@ -374,6 +398,7 @@ Feature: Step - has()
       | result |
       | v[marko] |
 
+  @TinyGremlin
   Scenario: g_V_hasXname_endingWithXasXX
     Given the modern graph
     And the traversal of
@@ -407,6 +432,7 @@ Feature: Step - has()
       | result |
       | v[marko] |
 
+  @TinyGremlin
   Scenario: g_V_hasXname_not_containingXarkXX
     Given the modern graph
     And the traversal of
@@ -422,6 +448,7 @@ Feature: Step - has()
       | v[ripple] |
       | v[peter] |
 
+  @TinyGremlin
   Scenario: g_V_hasXname_not_startingWithXmarXX
     Given the modern graph
     And the traversal of
@@ -437,6 +464,7 @@ Feature: Step - has()
       | v[ripple] |
       | v[peter] |
 
+  @TinyGremlin
   Scenario: g_V_hasXname_not_endingWithXasXX
     Given the modern graph
     And the traversal of
@@ -452,6 +480,7 @@ Feature: Step - has()
       | v[ripple] |
       | v[peter] |
 
+ @TinyGremlin
  Scenario: g_V_hasXname_regexXrMarXX
     Given the modern graph
     And the traversal of
@@ -463,6 +492,7 @@ Feature: Step - has()
       | result |
       | v[marko] |
 
+ @TinyGremlin
  Scenario: g_V_hasXname_notRegexXrMarXX
     Given the modern graph
     And the traversal of
@@ -478,6 +508,7 @@ Feature: Step - has()
       | v[ripple] |
       | v[peter] |
 
+  @TinyGremlin
   Scenario: g_V_hasXname_regexXTinkerXX
     Given the empty graph
     And the graph initializer of
@@ -493,6 +524,7 @@ Feature: Step - has()
       | result |
       | Apache TinkerPop© |
 
+  @TinyGremlin
   Scenario: g_V_hasXname_regexXTinkerUnicodeXX
     Given the empty graph
     And the graph initializer of
@@ -508,6 +540,7 @@ Feature: Step - has()
       | result |
       | Apache TinkerPop© |
 
+  @TinyGremlin
   Scenario: g_V_hasXp_neqXvXX
     Given the modern graph
     And the traversal of
@@ -541,6 +574,7 @@ Feature: Step - has()
       | v[marko] |
       | v[vadas] |
 
+  @TinyGremlin
   Scenario: g_V_hasXk_withinXcXX_valuesXkX
     Given the empty graph
     And the graph initializer of
@@ -558,6 +592,7 @@ Feature: Step - has()
       | ✦ |
       | ♠ |
 
+  @TinyGremlin
   Scenario: g_V_hasXnullX
     Given the modern graph
     And the traversal of
@@ -567,6 +602,7 @@ Feature: Step - has()
     When iterated to list
     Then the result should be empty
 
+  @TinyGremlin
   Scenario: g_V_hasXnull_testnullkeyX
     Given the modern graph
     And the traversal of
@@ -576,6 +612,7 @@ Feature: Step - has()
     When iterated to list
     Then the result should be empty
 
+  @TinyGremlin
   Scenario: g_E_hasXnullX
     Given the modern graph
     And the traversal of
@@ -585,6 +622,7 @@ Feature: Step - has()
     When iterated to list
     Then the result should be empty
 
+  @TinyGremlin
   Scenario: g_V_hasXlabel_personX
     Given the modern graph
     And the traversal of
@@ -599,6 +637,7 @@ Feature: Step - has()
       | v[josh] |
       | v[peter] |
 
+  @TinyGremlin
   Scenario: g_V_hasXlabel_eqXpersonXX
     Given the modern graph
     And the traversal of
@@ -613,6 +652,7 @@ Feature: Step - has()
       | v[josh] |
       | v[peter] |
 
+  @TinyGremlin
   Scenario: g_V_hasXname_nullX
     Given the modern graph
     And the traversal of
diff --git a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/filter/HasId.feature b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/filter/HasId.feature
index 818d55ec33..f942f920f7 100644
--- a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/filter/HasId.feature
+++ b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/filter/HasId.feature
@@ -63,6 +63,7 @@ Feature: Step - hasId()
       | result |
       | d[6].l |
 
+  @TinyGremlin
   Scenario: g_V_hasIdXnullX
     Given the modern graph
     And the traversal of
@@ -72,6 +73,7 @@ Feature: Step - hasId()
     When iterated to list
     Then the result should be empty
 
+  @TinyGremlin
   Scenario: g_V_hasIdXeqXnullXX
     Given the modern graph
     And the traversal of
@@ -81,6 +83,7 @@ Feature: Step - hasId()
     When iterated to list
     Then the result should be empty
 
+  @TinyGremlin
   Scenario: g_V_hasIdX2_nullX
     Given the modern graph
     And using the parameter vid2 defined as "v[vadas].id"
@@ -93,6 +96,7 @@ Feature: Step - hasId()
       | result |
       | v[vadas] |
 
+  @TinyGremlin
   Scenario: g_V_hasIdXmarkovar_vadasvarX
     Given the modern graph
     And using the parameter vid1 defined as "v[marko].id"
@@ -148,6 +152,7 @@ Feature: Step - hasId()
       | v[marko] |
       | v[vadas] |
 
+  @TinyGremlin
   Scenario: g_V_hasIdXnull_2X
     Given the modern graph
     And using the parameter vid2 defined as "v[vadas].id"
@@ -160,6 +165,7 @@ Feature: Step - hasId()
       | result |
       | v[vadas] |
 
+  @TinyGremlin
   Scenario: g_V_hasIdX1X_hasIdX2X
     Given the modern graph
     And using the parameter vid1 defined as "v[marko].id"
@@ -171,6 +177,7 @@ Feature: Step - hasId()
     When iterated to list
     Then the result should be empty
 
+  @TinyGremlin
   Scenario: g_V_in_hasIdXneqX1XX
     Given the modern graph
     And using the parameter xx1 defined as "v[marko].id"
@@ -185,6 +192,7 @@ Feature: Step - hasId()
       | v[josh] |
       | v[peter] |
 
+  @TinyGremlin
   Scenario: g_VX1X_out_hasIdX2X
     Given the modern graph
     And using the parameter vid1 defined as "v[marko].id"
@@ -198,6 +206,7 @@ Feature: Step - hasId()
       | result |
       | v[vadas] |
 
+  @TinyGremlin
   Scenario: g_VX1X_out_hasXid_2_3X
     Given the modern graph
     And using the parameter vid1 defined as "v[marko].id"
@@ -241,6 +250,7 @@ Feature: Step - hasId()
       | result |
       | v[vadas] |
 
+  @TinyGremlin
   Scenario: g_VX1X_out_hasXid_2_3X_inList
     Given the modern graph
     And using the parameter vid1 defined as "v[marko].id"
@@ -255,6 +265,7 @@ Feature: Step - hasId()
       | v[vadas] |
       | v[lop] |
 
+  @TinyGremlin
   Scenario: g_V_hasXid_1_2X
     Given the modern graph
     And using the parameter vid1 defined as "v[marko].id"
@@ -269,6 +280,7 @@ Feature: Step - hasId()
       | v[marko] |
       | v[vadas] |
 
+  @TinyGremlin
   Scenario: g_V_hasXid_1_2X_inList
     Given the modern graph
     And using the parameter xx1 defined as "l[v[marko].id,v[vadas].id]"
diff --git a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/filter/HasLabel.feature b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/filter/HasLabel.feature
index 54e29938ad..d7eb23c968 100644
--- a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/filter/HasLabel.feature
+++ b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/filter/HasLabel.feature
@@ -18,6 +18,7 @@
 @StepClassFilter @StepHasLabel
 Feature: Step - hasLabel()
 
+  @TinyGremlin
   Scenario: g_EX7X_hasLabelXknowsX
     Given the modern graph
     And using the parameter eid7 defined as "e[marko-knows->vadas].id"
@@ -30,6 +31,7 @@ Feature: Step - hasLabel()
       | result |
       | e[marko-knows->vadas] |
 
+  @TinyGremlin
   Scenario: g_E_hasLabelXknowsX
     Given the modern graph
     And the traversal of
@@ -42,7 +44,7 @@ Feature: Step - hasLabel()
       | e[marko-knows->vadas] |
       | e[marko-knows->josh] |
 
-  @MultiProperties @MetaProperties
+  @MultiProperties @MetaProperties @TinyGremlin
   Scenario: g_E_hasLabelXuses_traversesX
     Given the crew graph
     And the traversal of
@@ -62,6 +64,7 @@ Feature: Step - hasLabel()
       | e[matthias-uses->tinkergraph] |
       | e[gremlin-traverses->tinkergraph] |
 
+  @TinyGremlin
   Scenario: g_V_hasLabelXperson_software_blahX
     Given the modern graph
     And the traversal of
@@ -78,6 +81,7 @@ Feature: Step - hasLabel()
       | v[lop] |
       | v[ripple] |
 
+  @TinyGremlin
   Scenario: g_V_hasLabelXperson_softwarevarX
     Given the modern graph
     And using the parameter xx1 defined as "software"
@@ -95,6 +99,7 @@ Feature: Step - hasLabel()
       | v[lop] |
       | v[ripple] |
 
+  @TinyGremlin
   Scenario: g_V_hasLabelXpersonX_hasLabelXsoftwareX
     Given the modern graph
     And the traversal of
@@ -104,6 +109,7 @@ Feature: Step - hasLabel()
     When iterated to list
     Then the result should be empty
 
+  @TinyGremlin
   Scenario: g_V_hasLabelXpersonvarX_hasLabelXsoftwareX
     Given the modern graph
     And using the parameter xx1 defined as "person"
@@ -114,6 +120,7 @@ Feature: Step - hasLabel()
     When iterated to list
     Then the result should be empty
 
+  @TinyGremlin
   Scenario: g_V_hasLabelXpersonvar_softwarevarX
     Given the modern graph
     And using the parameter xx1 defined as "person"
@@ -144,6 +151,7 @@ Feature: Step - hasLabel()
       | vadas |
       | peter |
 
+  @TinyGremlin
   Scenario: g_V_hasLabelXnullX
     Given the modern graph
     And the traversal of
@@ -153,6 +161,7 @@ Feature: Step - hasLabel()
     When iterated to list
     Then the result should be empty
 
+  @TinyGremlin
   Scenario: g_V_hasXlabel_nullX
     Given the modern graph
     And the traversal of
@@ -162,6 +171,7 @@ Feature: Step - hasLabel()
     When iterated to list
     Then the result should be empty
 
+  @TinyGremlin
   Scenario: g_V_hasLabelXnull_nullX
     Given the modern graph
     And the traversal of
@@ -171,6 +181,7 @@ Feature: Step - hasLabel()
     When iterated to list
     Then the result should be empty
 
+  @TinyGremlin
   Scenario: g_V_hasLabelXnull_personX
     Given the modern graph
     And the traversal of
@@ -185,6 +196,7 @@ Feature: Step - hasLabel()
       | v[josh] |
       | v[peter] |
 
+  @TinyGremlin
   Scenario: g_E_hasLabelXnullX
     Given the modern graph
     And the traversal of
@@ -194,6 +206,7 @@ Feature: Step - hasLabel()
     When iterated to list
     Then the result should be empty
 
+  @TinyGremlin
   Scenario: g_E_hasXlabel_nullX
     Given the modern graph
     And the traversal of
diff --git a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/filter/HasNot.feature b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/filter/HasNot.feature
index 10a24e50fe..404e6f0130 100644
--- a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/filter/HasNot.feature
+++ b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/filter/HasNot.feature
@@ -18,6 +18,7 @@
 @StepClassFilter @StepHasNot
 Feature: Step - hasNot()
 
+  @TinyGremlin
   Scenario: g_V_hasNotXageX_name
     Given the modern graph
     And the traversal of
@@ -28,4 +29,18 @@ Feature: Step - hasNot()
     Then the result should be unordered
       | result |
       | lop |
-      | ripple |
\ No newline at end of file
+      | ripple |
+  @TinyGremlin
+  Scenario: g_V_hasNotXlangX_name
+    Given the modern graph
+    And the traversal of
+      """
+      g.V().hasNot("lang").values("name")
+      """
+    When iterated to list
+    Then the result should be unordered
+      | result |
+      | marko  |
+      | vadas  |
+      | josh   |
+      | peter  |
diff --git a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/filter/Range.feature b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/filter/Range.feature
index 3341bc3541..2bfb27dad0 100644
--- a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/filter/Range.feature
+++ b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/filter/Range.feature
@@ -18,6 +18,7 @@
 @StepClassFilter @StepRange
 Feature: Step - range()
 
+  @TinyGremlin
   Scenario: g_VX1X_out_limitX2X
     Given the modern graph
     And using the parameter vid1 defined as "v[marko].id"
@@ -33,6 +34,7 @@ Feature: Step - range()
       | v[lop] |
     And the result should have a count of 2
 
+  @TinyGremlin
   Scenario: g_VX1X_out_limitX2varX
     Given the modern graph
     And using the parameter vid1 defined as "v[marko].id"
@@ -64,6 +66,7 @@ Feature: Step - range()
       | v[ripple] |
     And the result should have a count of 3
 
+  @TinyGremlin
   Scenario: g_VX1X_outXknowsX_outEXcreatedX_rangeX0_1X_inV
     Given the modern graph
     And using the parameter vid1 defined as "v[marko].id"
@@ -78,6 +81,7 @@ Feature: Step - range()
       | v[ripple] |
     And the result should have a count of 1
 
+  @TinyGremlin
   Scenario: g_VX1X_outXknowsX_outXcreatedX_rangeX0_1X
     Given the modern graph
     And using the parameter vid1 defined as "v[marko].id"
@@ -92,6 +96,7 @@ Feature: Step - range()
       | v[ripple] |
     And the result should have a count of 1
 
+  @TinyGremlin
   Scenario: g_VX1X_outXcreatedX_inXcreatedX_rangeX1_3X
     Given the modern graph
     And using the parameter vid1 defined as "v[marko].id"
@@ -107,6 +112,7 @@ Feature: Step - range()
       | v[peter] |
     And the result should have a count of 2
 
+  @TinyGremlin
   Scenario: g_VX1X_outXcreatedX_inXcreatedX_rangeX1var_3varX
     Given the modern graph
     And using the parameter vid1 defined as "v[marko].id"
@@ -124,6 +130,7 @@ Feature: Step - range()
       | v[peter] |
     And the result should have a count of 2
 
+  @TinyGremlin
   Scenario: g_VX1X_outXcreatedX_inEXcreatedX_rangeX1_3X_outV
     Given the modern graph
     And using the parameter vid1 defined as "v[marko].id"
@@ -231,6 +238,7 @@ Feature: Step - range()
       | m[{"b":"josh"}] |
       | m[{"b":"josh"}] |
 
+  @TinyGremlin
   Scenario: g_V_hasLabelXpersonX_order_byXageX_skipX1X_valuesXnameX
     Given the modern graph
     And the traversal of
@@ -244,6 +252,7 @@ Feature: Step - range()
       | josh |
       | peter |
 
+  @TinyGremlin
   Scenario: g_V_hasLabelXpersonX_order_byXageX_skipX1varX_valuesXnameX
     Given the modern graph
     And using the parameter xx1 defined as "d[1].l"
@@ -293,6 +302,7 @@ Feature: Step - range()
       | result |
       | l[d[0.4].d,d[0.5].d,d[1.0].d,d[1.0].d] |
 
+  @TinyGremlin
   Scenario: g_V_hasLabelXpersonX_order_byXageX_valuesXnameX_skipX1X
     Given the modern graph
     And the traversal of
@@ -331,6 +341,7 @@ Feature: Step - range()
       | marko |
       | marko |
 
+  @TinyGremlin
   Scenario: g_V_rangeX2_1X
     Given the modern graph
     And the traversal of
@@ -341,6 +352,7 @@ Feature: Step - range()
     Then the traversal will raise an error with message containing text of "Not a legal range: [2, 1]"
 
   # iterated next variant of the previous test (which is iterated to list)
+  @TinyGremlin
   Scenario: g_V_rangeX3_2X
     Given the modern graph
     And the traversal of
@@ -436,6 +448,7 @@ Feature: Step - range()
       | result |
       | marko |
 
+  @TinyGremlin
   Scenario: g_VX5X_limitX1X_in_limitX1X_in_valuesXnameX
     Given the modern graph
     And using the parameter vid5 defined as "v[ripple].id"
@@ -497,6 +510,7 @@ Feature: Step - range()
       | result |
       | marko |
 
+  @TinyGremlin
   Scenario: g_VX5X_rangeX0_1X_in_rangeX0_1X_in_valuesXnameX
     Given the modern graph
     And using the parameter vid5 defined as "v[ripple].id"
diff --git a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/filter/Tail.feature b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/filter/Tail.feature
index 4ffc9420ca..b7b8be906c 100644
--- a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/filter/Tail.feature
+++ b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/filter/Tail.feature
@@ -30,6 +30,7 @@ Feature: Step - tail()
       | ripple |
       | vadas |
 
+  @TinyGremlin
   Scenario: g_V_valuesXnameX_order_tailX2X
     Given the modern graph
     And the traversal of
@@ -42,6 +43,7 @@ Feature: Step - tail()
       | ripple |
       | vadas |
 
+  @TinyGremlin
   Scenario: g_V_valuesXnameX_order_tailX2varX
     Given the modern graph
     And using the parameter xx1 defined as "d[2].l"
@@ -55,6 +57,7 @@ Feature: Step - tail()
       | ripple |
       | vadas |
 
+  @TinyGremlin
   Scenario: g_V_valuesXnameX_order_tail
     Given the modern graph
     And the traversal of
@@ -66,6 +69,7 @@ Feature: Step - tail()
       | result |
       | vadas |
 
+  @TinyGremlin
   Scenario: g_V_valuesXnameX_order_tailX7X
     Given the modern graph
     And the traversal of
diff --git a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/AddEdge.feature b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/AddEdge.feature
index 02c2be3d7b..edb14a836f 100644
--- a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/AddEdge.feature
+++ b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/AddEdge.feature
@@ -450,7 +450,7 @@ Feature: Step - addE()
     And the graph should return 3 for count of "g.V(vid1).outE(\"knows\")"
     And the graph should return 1 for count of "g.V(vid1).out(\"knows\").has(\"name\",\"peter\")"
 
-  @AllowNullPropertyValues
+  @AllowNullPropertyValues @TinyGremlin
   Scenario: g_addEXknowsXpropertyXweight_nullXfromXV_hasXname_markoXX_toXV_hasXname_vadasXX
     Given the empty graph
     And the graph initializer of
@@ -466,7 +466,7 @@ Feature: Step - addE()
     Then the result should have a count of 1
     And the graph should return 1 for count of "g.E().has(\"knows\",\"weight\",null)"
 
-  @AllowNullPropertyValues
+  @AllowNullPropertyValues @TinyGremlin
   Scenario: g_addEXknowsvarXpropertyXweight_nullXfromXV_hasXname_markoXX_toXV_hasXname_vadasXX
     Given the empty graph
     And the graph initializer of
@@ -515,6 +515,7 @@ Feature: Step - addE()
       | result |
       | d[0.5].d |
 
+  @TinyGremlin
   Scenario: g_addEXknowsX_fromXV_hasXname_markoXX_toXV_hasXname_vadasXX_propertyXweight_0_5X_addEXknowsX_fromXV_hasXname_markoXX
     Given the empty graph
     And the graph initializer of
diff --git a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/AddVertex.feature b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/AddVertex.feature
index 99e1492f56..2dd651c493 100644
--- a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/AddVertex.feature
+++ b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/AddVertex.feature
@@ -473,6 +473,7 @@ Feature: Step - addV()
       | result |
       | name |
 
+  @TinyGremlin
   Scenario: g_addV_propertyXlabel_personX
     Given the empty graph
     And the traversal of
@@ -605,7 +606,6 @@ Feature: Step - addV()
     Then the result should have a count of 1
     And the graph should return 0 for count of "g.V().hasLabel(\"foo\").values()"
 
-
   Scenario: g_addV_propertyXset_emptyX
     Given the empty graph
     And the traversal of
@@ -616,7 +616,7 @@ Feature: Step - addV()
     Then the result should have a count of 1
     And the graph should return 0 for count of "g.V().hasLabel(\"person\").values()"
 
-  @AllowNullPropertyValues
+  @AllowNullPropertyValues @TinyGremlin
   Scenario: g_addVXpersonX_propertyXname_joshX_propertyXage_nullX
     Given the empty graph
     And the traversal of
@@ -640,6 +640,35 @@ Feature: Step - addV()
     And the graph should return 1 for count of "g.V().has(\"person\",\"name\",\"marko\").properties(\"friendWeight\").has(\"acl\",null)"
     And the graph should return 1 for count of "g.V().has(\"person\",\"name\",\"marko\").properties(\"friendWeight\").count()"
 
+  @TinyGremlin
+  Scenario: g_V_hasXperson_name_aliceX_propertyXage_51X
+    Given the empty graph
+    And the graph initializer of
+      """
+      g.addV("person").property("name", "alice").property("age", 50)
+      """
+    And the traversal of
+      """
+      g.V().has("person","name","alice").property("age", 51)
+      """
+    When iterated to list
+    Then the result should have a count of 1
+    And the graph should return 0 for count of "g.V().has(\"person\",\"age\",50)"
+    And the graph should return 1 for count of "g.V().has(\"person\",\"age\",51)"
+
+  @TinyGremlin
+  Scenario: g_addVXpersonX_propertyXname_aliceX_propertyXage_30X_query
+    Given the empty graph
+    And the traversal of
+      """
+      g.addV("person").property("name", "alice").property("age", 30)
+      """
+    When iterated to list
+    Then the result should have a count of 1
+    And the graph should return 1 for count of "g.V().has(\"person\",\"name\",\"alice\")"
+    And the graph should return 1 for count of "g.V().has(\"person\",\"age\",30)"
+
+
   Scenario: g_V_hasXperson_name_aliceX_propertyXsingle_age_unionXage_constantX1XX_sumX
     Given the empty graph
     And the graph initializer of
diff --git a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/Edge.feature b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/Edge.feature
index 670476742a..64ce226b2b 100644
--- a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/Edge.feature
+++ b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/Edge.feature
@@ -18,6 +18,7 @@
 @StepClassMap @StepE
 Feature: Step - E(), inV(), outV(), bothV(), otherV()
 
+  @TinyGremlin
   Scenario: g_E
     Given the modern graph
     And the traversal of
@@ -34,6 +35,7 @@ Feature: Step - E(), inV(), outV(), bothV(), otherV()
       | e[josh-created->lop] |
       | e[josh-created->ripple] |
 
+  @TinyGremlin
   Scenario: g_EX11X
     Given the modern graph
     And using the parameter eid11 defined as "e[josh-created->lop].id"
@@ -58,6 +60,7 @@ Feature: Step - E(), inV(), outV(), bothV(), otherV()
       | result |
       | e[josh-created->lop] |
 
+  @TinyGremlin
   Scenario: g_EXeid7_eid11X
     Given the modern graph
     And using the parameter eid7 defined as "e[marko-knows->vadas].id"
@@ -72,6 +75,7 @@ Feature: Step - E(), inV(), outV(), bothV(), otherV()
       | e[marko-knows->vadas] |
       | e[josh-created->lop] |
 
+  @TinyGremlin
   Scenario: g_EXlistXeid7_eid11XX
     Given the modern graph
     And using the parameter xx1 defined as "l[e[marko-knows->vadas].id,e[josh-created->lop].id]"
@@ -85,6 +89,7 @@ Feature: Step - E(), inV(), outV(), bothV(), otherV()
       | e[marko-knows->vadas] |
       | e[josh-created->lop] |
 
+  @TinyGremlin
   Scenario: g_EXnullX
     Given the modern graph
     And the traversal of
@@ -94,6 +99,7 @@ Feature: Step - E(), inV(), outV(), bothV(), otherV()
     When iterated to list
     Then the result should be empty
 
+  @TinyGremlin
   Scenario: g_EXlistXnullXX
     Given the modern graph
     And using the parameter xx1 defined as "l[null]"
@@ -104,6 +110,7 @@ Feature: Step - E(), inV(), outV(), bothV(), otherV()
     When iterated to list
     Then the result should be empty
 
+  @TinyGremlin
   Scenario: g_EX11_nullX
     Given the modern graph
     And using the parameter eid11 defined as "e[josh-created->lop].id"
@@ -116,7 +123,7 @@ Feature: Step - E(), inV(), outV(), bothV(), otherV()
       | result |
       | e[josh-created->lop] |
 
-  @GraphComputerVerificationMidENotSupported
+  @GraphComputerVerificationMidENotSupported @TinyGremlin
   Scenario: g_V_EX11X
     Given the modern graph
     And using the parameter eid11 defined as "e[josh-created->lop].id"
@@ -134,7 +141,7 @@ Feature: Step - E(), inV(), outV(), bothV(), otherV()
       | e[josh-created->lop] |
       | e[josh-created->lop] |
 
-  @GraphComputerVerificationMidENotSupported
+  @GraphComputerVerificationMidENotSupported @TinyGremlin
   Scenario: g_EX11X_E
     Given the modern graph
     And using the parameter eid11 defined as "e[josh-created->lop].id"
@@ -152,7 +159,7 @@ Feature: Step - E(), inV(), outV(), bothV(), otherV()
       | e[josh-created->lop] |
       | e[josh-created->ripple] |
 
-  @GraphComputerVerificationMidENotSupported
+  @GraphComputerVerificationMidENotSupported @TinyGremlin
   Scenario: g_V_EXnullX
     Given the modern graph
     And the traversal of
@@ -162,7 +169,7 @@ Feature: Step - E(), inV(), outV(), bothV(), otherV()
     When iterated to list
     Then the result should be empty
 
-  @GraphComputerVerificationMidENotSupported
+  @GraphComputerVerificationMidENotSupported @TinyGremlin
   Scenario: g_V_EXlistXnullXX
     Given the modern graph
     And using the parameter xx1 defined as "l[null]"
@@ -202,6 +209,7 @@ Feature: Step - E(), inV(), outV(), bothV(), otherV()
     Then the result should have a count of 1
     And the graph should return 1 for count of "g.E().hasLabel(\"tests\")"
 
+  @TinyGremlin
   Scenario: g_VX1X_outE_inV
     Given the modern graph
     And using the parameter vid1 defined as "v[marko].id"
@@ -216,6 +224,7 @@ Feature: Step - E(), inV(), outV(), bothV(), otherV()
       | v[josh] |
       | v[lop] |
 
+  @TinyGremlin
   Scenario: g_VX2X_inE_outV
     Given the modern graph
     And using the parameter vid2 defined as "v[vadas].id"
@@ -228,6 +237,7 @@ Feature: Step - E(), inV(), outV(), bothV(), otherV()
       | result |
       | v[marko] |
 
+  @TinyGremlin
   Scenario: g_V_outE_hasXweight_1X_outV
     Given the modern graph
     And the traversal of
@@ -240,6 +250,7 @@ Feature: Step - E(), inV(), outV(), bothV(), otherV()
       | v[marko] |
       | v[josh] |
 
+  @TinyGremlin
   Scenario: g_VX1X_outE_otherV
     Given the modern graph
     And using the parameter vid1 defined as "v[marko].id"
@@ -254,6 +265,7 @@ Feature: Step - E(), inV(), outV(), bothV(), otherV()
       | v[josh] |
       | v[lop] |
 
+  @TinyGremlin
   Scenario: g_VX4X_bothE_otherV
     Given the modern graph
     And using the parameter vid4 defined as "v[josh].id"
@@ -268,6 +280,7 @@ Feature: Step - E(), inV(), outV(), bothV(), otherV()
       | v[ripple] |
       | v[lop] |
 
+  @TinyGremlin
   Scenario: g_VX4X_bothE_hasXweight_lt_1X_otherV
     Given the modern graph
     And using the parameter vid4 defined as "v[josh].id"
@@ -280,6 +293,7 @@ Feature: Step - E(), inV(), outV(), bothV(), otherV()
       | result |
       | v[lop] |
 
+  @TinyGremlin
   Scenario: get_g_VX1X_outE_otherV
     Given the modern graph
     And using the parameter vid1 defined as "v[marko].id"
@@ -294,6 +308,7 @@ Feature: Step - E(), inV(), outV(), bothV(), otherV()
       | v[josh] |
       | v[lop] |
 
+  @TinyGremlin
   Scenario: g_VX1X_outEXknowsX_inV
     Given the modern graph
     And using the parameter vid1 defined as "v[marko].id"
@@ -307,6 +322,7 @@ Feature: Step - E(), inV(), outV(), bothV(), otherV()
       | v[vadas] |
       | v[josh] |
 
+  @TinyGremlin
   Scenario: g_VX1X_outEXknows_createdX_inV
     Given the modern graph
     And using the parameter vid1 defined as "v[marko].id"
diff --git a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/ElementMap.feature b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/ElementMap.feature
index f119a2e964..999805cac0 100644
--- a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/ElementMap.feature
+++ b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/ElementMap.feature
@@ -18,6 +18,7 @@
 @StepClassMap @StepElementMap
 Feature: Step - elementMap()
 
+  @TinyGremlin
   Scenario: g_V_elementMap
     Given the modern graph
     And the traversal of
@@ -34,6 +35,7 @@ Feature: Step - elementMap()
       | m[{"t[id]": "v[lop].id", "t[label]": "software", "name": "lop", "lang": "java"}] |
       | m[{"t[id]": "v[ripple].id", "t[label]": "software", "name": "ripple", "lang": "java"}] |
 
+  @TinyGremlin
   Scenario: g_V_elementMapXname_ageX
     Given the modern graph
     And the traversal of
@@ -50,7 +52,7 @@ Feature: Step - elementMap()
       | m[{"t[id]": "v[lop].id", "t[label]": "software", "name": "lop"}] |
       | m[{"t[id]": "v[ripple].id", "t[label]": "software", "name": "ripple"}] |
 
-  @GraphComputerVerificationReferenceOnly
+  @GraphComputerVerificationReferenceOnly @TinyGremlin
   Scenario: g_EX11X_elementMap
     Given the modern graph
     And using the parameter eid11 defined as "e[josh-created->lop].id"
@@ -63,6 +65,7 @@ Feature: Step - elementMap()
       | result |
       | m[{"t[id]": "e[josh-created->lop].id", "t[label]": "created", "weight": "d[0.4].d", "D[OUT]": "m[{\\"t[id]\\": \\"v[josh].id\\", \\"t[label]\\": \\"person\\"}]", "D[IN]": "m[{\\"t[id]\\": \\"v[lop].id\\", \\"t[label]\\": \\"software\\"}]"}] |
 
+  @TinyGremlin
   Scenario: g_V_elementMapXname_age_nullX
     Given the modern graph
     And the traversal of
diff --git a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/Order.feature b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/Order.feature
index c49c6fca14..2780cecb6f 100644
--- a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/Order.feature
+++ b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/Order.feature
@@ -18,6 +18,7 @@
 @StepClassMap @StepOrder
 Feature: Step - order()
 
+  @TinyGremlin
   Scenario: g_V_name_order
     Given the modern graph
     And the traversal of
@@ -34,6 +35,7 @@ Feature: Step - order()
       | ripple |
       | vadas  |
 
+  @TinyGremlin
   Scenario: g_V_order_byXname_ascX_name
     Given the modern graph
     And the traversal of
@@ -50,6 +52,7 @@ Feature: Step - order()
       | ripple |
       | vadas  |
 
+  @TinyGremlin
   Scenario: g_V_order_byXnameX_name
     Given the modern graph
     And the traversal of
@@ -66,6 +69,7 @@ Feature: Step - order()
       | ripple |
       | vadas  |
 
+  @TinyGremlin
   Scenario: g_V_outE_order_byXweight_descX_weight
     Given the modern graph
     And the traversal of
@@ -96,6 +100,7 @@ Feature: Step - order()
       | m[{"a":"v[josh]","b":"v[ripple]"}] |
       | m[{"a":"v[josh]","b":"v[lop]"}] |
 
+  @TinyGremlin
   Scenario: g_V_both_hasLabelXpersonX_order_byXage_descX_limitX5X_name
     Given the modern graph
     And the traversal of
@@ -212,6 +217,7 @@ Feature: Step - order()
       | result |
       | l[v[vadas],v[marko],v[josh],v[peter]] |
 
+  @TinyGremlin
   Scenario: g_V_both_hasLabelXpersonX_order_byXage_descX_name
     Given the modern graph
     And the traversal of
@@ -246,6 +252,7 @@ Feature: Step - order()
       | v[ripple] |
       | v[vadas] |
 
+  @TinyGremlin
   Scenario: g_V_hasLabelXpersonX_order_byXageX
     Given the modern graph
     And the traversal of
@@ -548,4 +555,54 @@ Feature: Step - order()
     When iterated to list
     Then the result should be unordered
       | result |
-      | d[29].i |
\ No newline at end of file
+      | d[29].i |
+  @TinyGremlin
+  Scenario: g_V_order_byXTidX_name
+    Given the modern graph
+    And the traversal of
+      """
+      g.V().order().by(T.id).values("name")
+      """
+    When iterated to list
+    Then the result should be ordered
+      | result |
+      | marko  |
+      | vadas  |
+      | lop    |
+      | josh   |
+      | ripple |
+      | peter  |
+
+  @TinyGremlin
+  Scenario: g_V_order_byXTid_descX_name
+    Given the modern graph
+    And the traversal of
+      """
+      g.V().order().by(T.id, Order.desc).values("name")
+      """
+    When iterated to list
+    Then the result should be ordered
+      | result |
+      | peter  |
+      | ripple |
+      | josh   |
+      | lop    |
+      | vadas  |
+      | marko  |
+
+  @TinyGremlin
+  Scenario: g_V_order_byXTlabelX_label
+    Given the modern graph
+    And the traversal of
+      """
+      g.V().order().by(T.label).label()
+      """
+    When iterated to list
+    Then the result should be ordered
+      | result   |
+      | person   |
+      | person   |
+      | person   |
+      | person   |
+      | software |
+      | software |
diff --git a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/Path.feature b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/Path.feature
index 36722662a1..adfdd7ccb1 100644
--- a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/Path.feature
+++ b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/Path.feature
@@ -18,6 +18,7 @@
 @StepClassMap @StepPath
 Feature: Step - path()
 
+  @TinyGremlin
   Scenario: g_VX1X_name_path
     Given the modern graph
     And using the parameter vid1 defined as "v[marko].id"
@@ -82,6 +83,7 @@ Feature: Step - path()
       | result |
       | p[v[marko]] |
 
+  @TinyGremlin
   Scenario: g_VX1X_outEXcreatedX_inV_inE_outV_path
     Given the modern graph
     And using the parameter vid1 defined as "v[marko].id"
@@ -161,4 +163,17 @@ Feature: Step - path()
     Then the result should be unordered
       | result |
       | p[d[1].i] |
-      | p[null] |
\ No newline at end of file
+      | p[null] |
+  @TinyGremlin
+  Scenario: g_VX1X_out_out_path
+    Given the modern graph
+    And using the parameter vid1 defined as "v[marko].id"
+    And the traversal of
+      """
+      g.V(vid1).out().out().path()
+      """
+    When iterated to list
+    Then the result should be unordered
+      | result                          |
+      | p[v[marko],v[josh],v[ripple]] |
+      | p[v[marko],v[josh],v[lop]]    |
diff --git a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/Properties.feature b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/Properties.feature
index b5b40fee17..1be665bda6 100644
--- a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/Properties.feature
+++ b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/Properties.feature
@@ -88,6 +88,7 @@ Feature: Step - properties()
       | lop |
       | ripple |
 
+  @TinyGremlin
   Scenario: g_V_valuesXname_age_nullX
     Given the modern graph
     And the traversal of
diff --git a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/ValueMap.feature b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/ValueMap.feature
index 63f52324d3..62043514fa 100644
--- a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/ValueMap.feature
+++ b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/ValueMap.feature
@@ -18,6 +18,7 @@
 @StepClassMap @StepValueMap
 Feature: Step - valueMap()
 
+  @TinyGremlin
   Scenario: g_V_valueMap
     Given the modern graph
     And the traversal of
@@ -34,6 +35,7 @@ Feature: Step - valueMap()
       | m[{"name": ["lop"], "lang": ["java"]}] |
       | m[{"name": ["ripple"], "lang": ["java"]}] |
 
+  @TinyGremlin
   Scenario: g_V_valueMapXtrueX
     Given the modern graph
     And the traversal of
@@ -66,6 +68,7 @@ Feature: Step - valueMap()
       | m[{"t[id]": "v[lop].id", "t[label]": "software", "name": ["lop"], "lang": ["java"]}] |
       | m[{"t[id]": "v[ripple].id", "t[label]": "software", "name": ["ripple"], "lang": ["java"]}] |
 
+  @TinyGremlin
   Scenario: g_V_valueMapXname_ageX
     Given the modern graph
     And the traversal of
@@ -82,6 +85,7 @@ Feature: Step - valueMap()
       | m[{"name": ["lop"]}] |
       | m[{"name": ["ripple"]}] |
 
+  @TinyGremlin
   Scenario: g_V_valueMapXtrue_name_ageX
     Given the modern graph
     And the traversal of
@@ -146,6 +150,7 @@ Feature: Step - valueMap()
       | m[{"t[id]": "v[lop].id", "name": "lop"}] |
       | m[{"t[id]": "v[ripple].id", "name": "ripple"}] |
 
+  @TinyGremlin
   Scenario: g_VX1X_outXcreatedX_valueMap
     Given the modern graph
     And using the parameter vid1 defined as "v[marko].id"
@@ -195,6 +200,7 @@ Feature: Step - valueMap()
     When iterated next
     Then the traversal will raise an error with message containing text of "step can only have one by modulator"
 
+  @TinyGremlin
   Scenario: g_V_valueMapXname_age_nullX
     Given the modern graph
     And the traversal of
diff --git a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/Vertex.feature b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/Vertex.feature
index f9218ee462..91f6f65b06 100644
--- a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/Vertex.feature
+++ b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/Vertex.feature
@@ -18,6 +18,7 @@
 @StepClassMap @StepVertex
 Feature: Step - V(), out(), in(), both(), inE(), outE(), bothE()
 
+  @TinyGremlin
   Scenario: g_VXnullX
     Given the modern graph
     And the traversal of
@@ -27,6 +28,7 @@ Feature: Step - V(), out(), in(), both(), inE(), outE(), bothE()
     When iterated to list
     Then the result should be empty
 
+  @TinyGremlin
   Scenario: g_VXlistXnullXX
     Given the modern graph
     And using the parameter xx1 defined as "l[null]"
@@ -37,6 +39,7 @@ Feature: Step - V(), out(), in(), both(), inE(), outE(), bothE()
     When iterated to list
     Then the result should be empty
 
+  @TinyGremlin
   Scenario: g_VX1_nullX
     Given the modern graph
     And using the parameter vid1 defined as "v[marko].id"
@@ -49,6 +52,7 @@ Feature: Step - V(), out(), in(), both(), inE(), outE(), bothE()
       | result |
       | v[marko] |
 
+  @TinyGremlin
   Scenario: g_VXlistX1_2_3XX_name
     Given the modern graph
     And using the parameter xx1 defined as "l[v[marko].id,v[vadas].id,v[lop].id]"
@@ -63,6 +67,7 @@ Feature: Step - V(), out(), in(), both(), inE(), outE(), bothE()
       | vadas |
       | lop |
 
+  @TinyGremlin
   Scenario: g_VXlistXv1_v2_v3XX_name
     Given the modern graph
     And using the parameter xx1 defined as "l[v[marko].id,v[vadas].id,v[lop].id]"
@@ -77,6 +82,7 @@ Feature: Step - V(), out(), in(), both(), inE(), outE(), bothE()
       | vadas |
       | lop |
 
+  @TinyGremlin
   Scenario: g_V
     Given the modern graph
     And the traversal of
@@ -93,6 +99,7 @@ Feature: Step - V(), out(), in(), both(), inE(), outE(), bothE()
       | v[ripple] |
       | v[peter] |
 
+  @TinyGremlin
   Scenario: g_VXv1X_out
     Given the modern graph
     And using the parameter vid1 defined as "v[marko].id"
@@ -107,6 +114,7 @@ Feature: Step - V(), out(), in(), both(), inE(), outE(), bothE()
       | v[lop] |
       | v[josh] |
 
+  @TinyGremlin
   Scenario: g_VX1X_out
     Given the modern graph
     And using the parameter vid1 defined as "v[marko].id"
@@ -121,6 +129,7 @@ Feature: Step - V(), out(), in(), both(), inE(), outE(), bothE()
       | v[lop] |
       | v[josh] |
 
+  @TinyGremlin
   Scenario: g_VX2X_in
     Given the modern graph
     And using the parameter vid2 defined as "v[vadas].id"
@@ -133,6 +142,7 @@ Feature: Step - V(), out(), in(), both(), inE(), outE(), bothE()
       | result |
       | v[marko] |
 
+  @TinyGremlin
   Scenario: g_VX4X_both
     Given the modern graph
     And using the parameter vid4 defined as "v[josh].id"
@@ -147,6 +157,7 @@ Feature: Step - V(), out(), in(), both(), inE(), outE(), bothE()
       | v[lop] |
       | v[ripple] |
 
+  @TinyGremlin
   Scenario: g_VX1X_outE
     Given the modern graph
     And using the parameter vid1 defined as "v[marko].id"
@@ -161,6 +172,7 @@ Feature: Step - V(), out(), in(), both(), inE(), outE(), bothE()
       | e[marko-knows->josh] |
       | e[marko-knows->vadas] |
 
+  @TinyGremlin
   Scenario: g_VX2X_outE
     Given the modern graph
     And using the parameter vid2 defined as "v[vadas].id"
@@ -173,6 +185,7 @@ Feature: Step - V(), out(), in(), both(), inE(), outE(), bothE()
       | result |
       | e[marko-knows->vadas] |
 
+  @TinyGremlin
   Scenario: g_VX4X_bothEXcreatedX
     Given the modern graph
     And using the parameter vid4 defined as "v[josh].id"
@@ -186,6 +199,7 @@ Feature: Step - V(), out(), in(), both(), inE(), outE(), bothE()
       | e[josh-created->lop] |
       | e[josh-created->ripple] |
 
+  @TinyGremlin
   Scenario: g_VX4X_bothEXcreatedvarX
     Given the modern graph
     And using the parameter vid4 defined as "v[josh].id"
@@ -200,6 +214,7 @@ Feature: Step - V(), out(), in(), both(), inE(), outE(), bothE()
       | e[josh-created->lop] |
       | e[josh-created->ripple] |
 
+  @TinyGremlin
   Scenario: g_VX4X_bothE
     Given the modern graph
     And using the parameter vid4 defined as "v[josh].id"
@@ -214,6 +229,7 @@ Feature: Step - V(), out(), in(), both(), inE(), outE(), bothE()
       | e[josh-created->ripple] |
       | e[marko-knows->josh] |
 
+  @TinyGremlin
   Scenario: g_V_out_outE_inV_inE_inV_both_name
     Given the modern graph
     And the traversal of
@@ -234,6 +250,7 @@ Feature: Step - V(), out(), in(), both(), inE(), outE(), bothE()
       | peter |
       | peter |
 
+  @TinyGremlin
   Scenario: g_VX2X_inE
     Given the modern graph
     And using the parameter vid2 defined as "v[vadas].id"
@@ -246,6 +263,7 @@ Feature: Step - V(), out(), in(), both(), inE(), outE(), bothE()
       | result |
       | e[marko-knows->vadas] |
 
+  @TinyGremlin
   Scenario: g_VX1X_outXknowsX
     Given the modern graph
     And using the parameter vid1 defined as "v[marko].id"
@@ -272,6 +290,7 @@ Feature: Step - V(), out(), in(), both(), inE(), outE(), bothE()
       | v[vadas] |
       | v[josh] |
 
+  @TinyGremlin
   Scenario: g_VX1X_outXknows_createdX
     Given the modern graph
     And using the parameter vid1 defined as "v[marko].id"
@@ -286,6 +305,7 @@ Feature: Step - V(), out(), in(), both(), inE(), outE(), bothE()
       | v[josh] |
       | v[lop] |
 
+  @TinyGremlin
   Scenario: g_VX1X_outXknowsvar_createdvarX
     Given the modern graph
     And using the parameter vid1 defined as "v[marko].id"
@@ -302,6 +322,7 @@ Feature: Step - V(), out(), in(), both(), inE(), outE(), bothE()
       | v[josh] |
       | v[lop] |
 
+  @TinyGremlin
   Scenario: g_V_out_out
     Given the modern graph
     And the traversal of
@@ -314,6 +335,7 @@ Feature: Step - V(), out(), in(), both(), inE(), outE(), bothE()
       | v[ripple] |
       | v[lop] |
 
+  @TinyGremlin
   Scenario: g_VX1X_out_out_out
     Given the modern graph
     And using the parameter vid1 defined as "v[marko].id"
@@ -324,6 +346,7 @@ Feature: Step - V(), out(), in(), both(), inE(), outE(), bothE()
     When iterated to list
     Then the result should be empty
 
+  @TinyGremlin
   Scenario: g_VX1X_out_name
     Given the modern graph
     And using the parameter vid1 defined as "v[marko].id"
@@ -383,7 +406,7 @@ Feature: Step - V(), out(), in(), both(), inE(), outE(), bothE()
     Then the result should be empty
     And the graph should return 3 for count of "g.V(vid1, vid2, vid3, vid4)"
 
-  @GraphComputerVerificationMidVNotSupported
+  @GraphComputerVerificationMidVNotSupported @TinyGremlin
   Scenario: g_V_hasLabelXpersonX_V_hasLabelXsoftwareX_name
     Given the modern graph
     And the traversal of
@@ -402,6 +425,7 @@ Feature: Step - V(), out(), in(), both(), inE(), outE(), bothE()
       | ripple |
       | ripple |
 
+  @TinyGremlin
   Scenario: g_V_hasLabelXloopsX_bothEXselfX
     Given the sink graph
     And the traversal of
@@ -414,6 +438,7 @@ Feature: Step - V(), out(), in(), both(), inE(), outE(), bothE()
       | e[loop-self->loop] |
       | e[loop-self->loop] |
 
+  @TinyGremlin
   Scenario: g_V_hasLabelXloopsX_bothXselfX
     Given the sink graph
     And the traversal of
@@ -449,7 +474,7 @@ Feature: Step - V(), out(), in(), both(), inE(), outE(), bothE()
       | result |
       | v[marko] |
 
-  @GraphComputerVerificationMidENotSupported
+  @GraphComputerVerificationMidENotSupported @TinyGremlin
   Scenario: g_VX1X_V_valuesXnameX
     Given the modern graph
     And using the parameter vid1 defined as "v[marko].id"
@@ -467,7 +492,7 @@ Feature: Step - V(), out(), in(), both(), inE(), outE(), bothE()
       | ripple |
       | peter  |
 
-  @GraphComputerVerificationMidENotSupported
+  @GraphComputerVerificationMidENotSupported @TinyGremlin
   Scenario: g_V_outXknowsX_V_name
     Given the modern graph
     And the traversal of
diff --git a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/semantics/Orderability.feature b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/semantics/Orderability.feature
index 1e1a3c3397..e6601fb2ba 100644
--- a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/semantics/Orderability.feature
+++ b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/semantics/Orderability.feature
@@ -18,6 +18,7 @@
 @StepClassSemantics
 Feature: Orderability
 
+  @TinyGremlin
   Scenario: g_V_values_order
     Given the modern graph
     And the traversal of


(tinkerpop) 03/06: Document Tiny Gremlin in the gremlin-javascript reference

Posted by sp...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

spmallette pushed a commit to branch jsprocess
in repository https://gitbox.apache.org/repos/asf/tinkerpop.git

commit eb339391b45cb6ce172f82874a3cbecf27c5da70
Author: Stephen Mallette <st...@amazon.com>
AuthorDate: Sun Jun 28 19:16:16 2026 -0400

    Document Tiny Gremlin in the gremlin-javascript reference
    
    Add a Tiny Gremlin section to the Gremlin-JavaScript variant docs covering local
    in-process execution: building and mutating a Graph, querying it with the supported
    step subset and the forEach()/reduce() terminals, post-processing results with
    JavaScript array methods for steps outside the subset, and the subgraph() workflow.
    
    Assisted-by: Claude Code:claude-opus-4-8
---
 docs/src/reference/gremlin-variants.asciidoc | 95 ++++++++++++++++++++++++++++
 1 file changed, 95 insertions(+)

diff --git a/docs/src/reference/gremlin-variants.asciidoc b/docs/src/reference/gremlin-variants.asciidoc
index 941e2e83dd..c87d707cb9 100644
--- a/docs/src/reference/gremlin-variants.asciidoc
+++ b/docs/src/reference/gremlin-variants.asciidoc
@@ -2185,6 +2185,101 @@ Promise.all([
 });
 ----
 
+[[gremlin-javascript-tiny-gremlin]]
+=== Tiny Gremlin
+
+Tiny Gremlin executes a defined subset of Gremlin locally and in-process against a detached `Graph` held in
+memory. No Gremlin Server or remote connection is involved. A local traversal source is created by passing a
+`Graph` instance to `with_()` in place of a `DriverRemoteConnection`:
+
+[source,javascript]
+----
+const Graph = gremlin.structure.Graph;
+
+const graph = new Graph();
+const g = traversal().with_(graph);
+----
+
+The supported steps and the precise semantics of each, including the `by()` modulators available on `path()`, are
+described in the link:https://tinkerpop.apache.org/docs/x.y.z/dev/provider/#tiny-gremlin[Tiny Gremlin] provider
+documentation. A step outside the subset raises a `LocalExecutionError`, which is exported from the `driver`
+namespace.
+
+==== Building a Graph
+
+A `Graph` is populated with the mutation steps `addV()`, `addE()` and `property()`. Each traversal is terminated
+with `iterate()` so that its mutation is applied to the in-memory graph:
+
+[source,javascript]
+----
+const graph = new Graph();
+const g = traversal().with_(graph);
+
+await g.addV('person').property('name', 'marko').property('age', 29).iterate();
+await g.addV('person').property('name', 'vadas').property('age', 27).iterate();
+await g.addE('knows')
+  .from_(__.V().has('name', 'marko'))
+  .to(__.V().has('name', 'vadas'))
+  .property('weight', 0.5)
+  .iterate();
+----
+
+==== Querying a Graph
+
+Once a `Graph` holds data, it is navigated and filtered with the supported read steps. Alongside the usual
+terminal methods such as `toList()` and `next()`, Tiny Gremlin adds two terminals that consume results with a
+native JavaScript function, `forEach()` and `reduce()`:
+
+[source,javascript]
+----
+// list the names reachable from marko
+const friends = await g.V().has('name', 'marko').out('knows').values('name').toList();
+
+// iterate results with a side-effecting function
+await g.V().hasLabel('person').values('name').forEach((name) => console.log(name));
+
+// fold results into a single value with a seeded accumulator
+const totalAge = await g.V().hasLabel('person').values('age').reduce((acc, age) => acc + age, 0);
+
+// path() projects each element through by()
+const paths = await g.V().has('name', 'marko').out().path().by('name').toList();
+----
+
+==== Working within the subset
+
+Because Tiny Gremlin is a subset, reshaping and reducing steps such as `project()`, `group()`, `groupCount()` and
+`sum()` are not available. The intended pattern is to perform everything the subset supports within the traversal,
+including sorting with `order().by()`, terminate with `toList()`, and then complete the work in JavaScript with
+the standard array methods:
+
+[source,javascript]
+----
+// project() is unavailable: order in the traversal, then map() the materialized result
+const people = (await g.V().hasLabel('person').order().by('name').elementMap().toList())
+  .map((m) => ({ name: m.get('name'), age: m.get('age') }));
+
+// groupCount() is unavailable: tally the labels with Array.reduce()
+const counts = (await g.V().label().toList())
+  .reduce((acc, label) => { acc[label] = (acc[label] || 0) + 1; return acc; }, {});
+----
+
+==== Processing a subgraph
+
+The primary use case for Tiny Gremlin is the local exploration of a subgraph extracted from a remote graph. The
+`subgraph()`-step returns a detached `Graph` to the client, and passing that `Graph` to `with_()` makes its
+contents available for in-process traversal without further calls to the server:
+
+[source,javascript]
+----
+// pull a subgraph from a remote graph
+const remote = traversal().with_(new DriverRemoteConnection('http://localhost:8182/gremlin'));
+const sg = (await remote.E().hasLabel('created').subgraph('sg').cap('sg').next()).value;
+
+// query the detached subgraph locally
+const lg = traversal().with_(sg);
+const creators = await lg.V().hasLabel('software').in_('created').values('name').toList();
+----
+
 [[gremlin-javascript-gvalue]]
 === GValue Parameterization
 


(tinkerpop) 04/06: Minor docs improvements for Tiny Gremlin

Posted by sp...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

spmallette pushed a commit to branch jsprocess
in repository https://gitbox.apache.org/repos/asf/tinkerpop.git

commit 9aa6339230fbc88230399a1a7ce90f9627db2a1a
Author: Stephen Mallette <st...@amazon.com>
AuthorDate: Mon Jun 29 10:06:39 2026 -0400

    Minor docs improvements for Tiny Gremlin
---
 docs/src/reference/gremlin-variants.asciidoc |  6 ++--
 docs/src/reference/the-traversal.asciidoc    | 42 ++++------------------------
 2 files changed, 9 insertions(+), 39 deletions(-)

diff --git a/docs/src/reference/gremlin-variants.asciidoc b/docs/src/reference/gremlin-variants.asciidoc
index c87d707cb9..4256fe0a32 100644
--- a/docs/src/reference/gremlin-variants.asciidoc
+++ b/docs/src/reference/gremlin-variants.asciidoc
@@ -2188,9 +2188,9 @@ Promise.all([
 [[gremlin-javascript-tiny-gremlin]]
 === Tiny Gremlin
 
-Tiny Gremlin executes a defined subset of Gremlin locally and in-process against a detached `Graph` held in
-memory. No Gremlin Server or remote connection is involved. A local traversal source is created by passing a
-`Graph` instance to `with_()` in place of a `DriverRemoteConnection`:
+<<tiny-gremlin-traversal, Tiny Gremlin>> is a subset of the Gremlin language that can execute locally and in-process
+against a detached `Graph` held in memory. No Gremlin Server or remote connection is involved. A local traversal source
+is created by passing a `Graph` instance to `with_()` in place of a `DriverRemoteConnection`:
 
 [source,javascript]
 ----
diff --git a/docs/src/reference/the-traversal.asciidoc b/docs/src/reference/the-traversal.asciidoc
index 8465103f13..844bdfa8bd 100644
--- a/docs/src/reference/the-traversal.asciidoc
+++ b/docs/src/reference/the-traversal.asciidoc
@@ -6865,19 +6865,20 @@ script = t.gremlin_lang.get_gremlin()
 [[tiny-gremlin-traversal]]
 == Tiny Gremlin
 
-*Tiny Gremlin* is a defined subset of the Gremlin traversal language that executes locally and in-process against a
+Tiny Gremlin is a defined subset of the Gremlin traversal language that executes locally and in-process against a
 small, detached `Graph` instance. The most common use case is to call `subgraph()` on a remote graph, receive a `Graph`
 object, and then navigate and filter it locally using familiar Gremlin syntax without an additional network round-trip.
 A secondary use case is constructing small graphs entirely in-process using `addV()`, `addE()`, and `property()`.
 
 Full details on semantics, supported forms, and limitations are covered in the
 link:https://tinkerpop.apache.org/docs/x.y.z/dev/provider/#tiny-gremlin[Tiny Gremlin Semantics] section of the
-Provider Documentation. Tiny Gremlin is currently only supported in `gremlin-javascript`.
+Provider Documentation. Tiny Gremlin is currently only supported in <<gremlin-javascript-tiny-gremlin,Javascript>>
 
 === Supported Steps
 
-Tiny Gremlin supports exactly 33 steps. No other step may be used in a Tiny Gremlin traversal; attempting to do so
-results in an error before any graph data is accessed.
+Tiny Gremlin supports the basic steps required to navigate, filter, mutate and extract values from elements in a graph.
+These steps should provide the basics required to author simple traversals for data gathering, path finding and other
+graph analysis tasks.
 
 [width="100%",options="header"]
 |===
@@ -6890,36 +6891,5 @@ results in an error before any graph data is accessed.
 |Range |<<limit-step,`limit()`>>, <<range-step,`range()`>>, <<skip-step,`skip()`>>, <<tail-step,`tail()`>>
 |Ordering |<<order-step,`order()`>>
 |Mutation |<<addvertex-step,`addV()`>>, <<addedge-step,`addE()`>>, <<property-step,`property()`>>
-|Modulators |<<from-step,`from()`>>, <<to-step,`to()`>>
+|Modulators |<<from-step,`from()`>>, <<to-step,`to()`>> - in conjunction with `addE()`, not `path()`.
 |===
-
-=== Language Support
-
-Tiny Gremlin is currently only implemented in `gremlin-javascript`. The `with_(graph)` API accepts a local `Graph`
-object in place of a remote connection, transparently enabling local traversal using the same Gremlin API:
-
-[source,javascript]
-----
-const { graph: { Graph }, process: { anonymousTraversal: { traversal } } } = require('gremlin');
-
-const graph = new Graph();
-const g = traversal().with_(graph);
-
-// Build a small graph and query it locally
-const names = await g.addV('person').property('name', 'alice')
-                      .V().values('name').toList();
-// Returns: ['alice']
-----
-
-The primary use case of processing a subgraph received from a remote server requires no code changes other than
-passing the returned `Graph` object to `with_()`:
-
-[source,javascript]
-----
-// Retrieve a subgraph from a remote server
-const subgraph = await gRemote.V().hasLabel('person').subgraph('sg').cap('sg').next();
-
-// Query it locally with Tiny Gremlin — no additional network calls
-const g = traversal().with_(subgraph);
-const paths = await g.V().out('knows').in('knows').path().toList();
-----


(tinkerpop) 06/06: Add is() and loops() to gremlin-javascript Tiny Gremlin

Posted by sp...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

spmallette pushed a commit to branch jsprocess
in repository https://gitbox.apache.org/repos/asf/tinkerpop.git

commit 84586ac66794a57031c16d1d4ae8b6e9e382b5d3
Author: Stephen Mallette <st...@amazon.com>
AuthorDate: Tue Jun 30 12:33:26 2026 -0400

    Add is() and loops() to gremlin-javascript Tiny Gremlin
    
    loops() reports the enclosing repeat()'s iteration count and pairs with is()
    to bound a loop, e.g. until(__.loops().is(2)). Also restores the dropped
    Tiny Gremlin changelog entry.
    
    Assisted-by: Claude Code:claude-opus-4-8
---
 CHANGELOG.asciidoc                                 |  2 +
 docs/src/dev/provider/tiny-gremlin.asciidoc        | 30 +++++--
 .../lib/language/executor/ExecuteVisitor.ts        | 18 +++++
 .../lib/process/local/LocalExecutor.ts             | 28 +++++--
 .../gremlin-javascript/lib/process/local/passes.ts |  4 +-
 .../gremlin-javascript/lib/process/local/steps.ts  | 67 +++++++++++----
 .../gremlin-javascript/lib/process/local/types.ts  |  8 ++
 .../test/unit/tiny-gremlin-is-loops-test.js        | 94 ++++++++++++++++++++++
 .../gremlin/test/features/branch/Repeat.feature    |  1 +
 .../gremlin/test/features/filter/Is.feature        |  6 ++
 10 files changed, 230 insertions(+), 28 deletions(-)

diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc
index 963ad54f5c..85472872c1 100644
--- a/CHANGELOG.asciidoc
+++ b/CHANGELOG.asciidoc
@@ -130,6 +130,8 @@ image::https://raw.githubusercontent.com/apache/tinkerpop/master/docs/static/ima
 * Added the `gql-gremlin` module containing the TinkerGQL engine, a `MATCH`-pattern executor implementing a deliberate minimal subset of ISO GQL, usable by any graph provider.
 * Added `Graph.countVerticesByLabel(String)`, `Graph.countEdgesByLabel(String)`, and the `Graph.Index` nested interface as performance-hint extension points for the TinkerGQL engine.
 * Added GQL support to TinkerGraph with `gql-gremlin`,
+* Added Tiny Gremlin semantics which provider a defined subset of Gremlin steps (navigation, filtering, value extraction, path tracking, range, ordering, looping, and local mutation) for use as an official lightweight Gremlin implementation.
+* Implemented Tiny Gremlin for Typescript.
 
 [[release-4-0-0-beta-2]]
 === TinkerPop 4.0.0-beta.2 (April 1, 2026)
diff --git a/docs/src/dev/provider/tiny-gremlin.asciidoc b/docs/src/dev/provider/tiny-gremlin.asciidoc
index 1bbb310827..c7a6f94dd5 100644
--- a/docs/src/dev/provider/tiny-gremlin.asciidoc
+++ b/docs/src/dev/provider/tiny-gremlin.asciidoc
@@ -42,7 +42,7 @@ deliberately excluded. The result is a small, self-contained, predictable execut
 
 === Supported Steps
 
-Tiny Gremlin supports exactly the 37 steps listed below, grouped by category. No other step may be used in a Tiny
+Tiny Gremlin supports exactly the 39 steps listed below, grouped by category. No other step may be used in a Tiny
 Gremlin traversal; doing so results in an error before any graph data is accessed.
 
 [width="100%",options="header"]
@@ -50,12 +50,12 @@ Gremlin traversal; doing so results in an error before any graph data is accesse
 |Category |Steps
 |Source |`V()`, `E()`
 |Navigation |`out()`, `in()`, `both()`, `outE()`, `inE()`, `bothE()`, `outV()`, `inV()`, `otherV()`
-|Filter |`has()`, `hasId()`, `hasLabel()`, `hasNot()`
+|Filter |`has()`, `hasId()`, `hasLabel()`, `hasNot()`, `is()`
 |Value Extraction |`id()`, `label()`, `values()`, `valueMap()`, `elementMap()`, `value()`, `key()`
 |Path |`path()`
 |Range |`limit()`, `range()`, `skip()`, `tail()`
 |Ordering |`order()`
-|Branch |`repeat()`
+|Branch |`repeat()`, `loops()`
 |Mutation |`addV()`, `addE()`, `property()`
 |Modulators |`from()`, `to()`, `times()`, `until()`, `emit()`
 |===
@@ -228,6 +228,14 @@ Maps a vertex to its incoming edges.
 
 Maps an edge to its incoming vertex (the head of the arrow).
 
+=== is()
+
+*Gremlin Semantics reference:* <<is-step,is()>>
+
+Filters the stream, keeping only traversers whose value equals the given object or satisfies the given predicate, for
+example `is(32)` or `is(P.gte(29))`. It pairs naturally with `loops()` to bound a `repeat()`, as in
+`until(__.loops().is(2))`.
+
 === key()
 
 *Gremlin Semantics reference:* <<key-step,key()>>
@@ -250,6 +258,18 @@ Truncates the result stream to at most `n` elements.
 
 * The `Scope` form is not supported and will raise an error.
 
+=== loops()
+
+*Gremlin Semantics reference:* <<loops-step,loops()>>
+
+Maps each traverser to the loop count of the nearest enclosing `repeat()` — zero on the first body pass, incrementing
+each iteration. It is most often used as an exit condition with `is()`, so `repeat(traversal).until(__.loops().is(2))`
+is equivalent to `repeat(traversal).times(2)`. Outside any `repeat()` it is zero.
+
+*Tiny Gremlin limitations:*
+
+* The `loops("label")` form, which references a labeled loop, is not supported because Tiny Gremlin has no step labels.
+
 === order()
 
 *Gremlin Semantics reference:* <<order-step,order()>>
@@ -365,8 +385,8 @@ modulator relative to `repeat()` determines its semantics, matching full Gremlin
   Declared after `repeat()` it emits traversers leaving the body, and declared before it emits them entering the body.
 
 A `condition` for `until()` or `emit()` is an anonymous filter traversal, satisfied when it produces at least one
-result. The loop body and these conditions may themselves contain any supported step, including `order().by()` or
-`path()`.
+result. The loop body and these conditions may themselves contain any supported step, including `order().by()`,
+`path()`, or `loops()` — so `until(__.loops().is(2))` bounds the loop by iteration count, equivalent to `times(2)`.
 
 The loop is driven breadth-first over the whole frontier: at each loop iteration every surviving traverser advances
 in lockstep through one shared evaluation of the body. A barrier step such as `order().by()` inside the body
diff --git a/gremlin-js/gremlin-javascript/lib/language/executor/ExecuteVisitor.ts b/gremlin-js/gremlin-javascript/lib/language/executor/ExecuteVisitor.ts
index 84b77dc3e4..e593bef2a7 100644
--- a/gremlin-js/gremlin-javascript/lib/language/executor/ExecuteVisitor.ts
+++ b/gremlin-js/gremlin-javascript/lib/language/executor/ExecuteVisitor.ts
@@ -516,6 +516,24 @@ export class ExecuteVisitor {
     this.push('emit', [this.recoverPredicate(ctx.traversalPredicate())]);
   }
 
+  visitTraversalMethod_loops_Empty(_ctx: any): void {
+    this.push('loops', []);
+  }
+
+  // loops(String) — the loop-label reference. Tiny Gremlin has no step labels, so the
+  // label is carried through for the executor to reject with a clear message.
+  visitTraversalMethod_loops_String(ctx: any): void {
+    this.push('loops', [this.recoverStringLiteral(ctx.stringLiteral())]);
+  }
+
+  visitTraversalMethod_is_Object(ctx: any): void {
+    this.push('is', [this.recoverGenericArg(ctx.genericArgument())]);
+  }
+
+  visitTraversalMethod_is_P(ctx: any): void {
+    this.push('is', [this.recoverPredicate(ctx.traversalPredicate())]);
+  }
+
   // ── Mutation steps ────────────────────────────────────────────────────────
 
   visitTraversalMethod_addV_Empty(ctx: any): void {
diff --git a/gremlin-js/gremlin-javascript/lib/process/local/LocalExecutor.ts b/gremlin-js/gremlin-javascript/lib/process/local/LocalExecutor.ts
index a37f25ebff..18090527c5 100644
--- a/gremlin-js/gremlin-javascript/lib/process/local/LocalExecutor.ts
+++ b/gremlin-js/gremlin-javascript/lib/process/local/LocalExecutor.ts
@@ -24,9 +24,9 @@ import { Graph, Vertex, Edge } from '../../structure/graph.js';
 import {
   wrap, getValue, NON_PRODUCTIVE, type StreamItem,
   stepOut, stepIn, stepBoth, stepOutE, stepInE, stepBothE, stepOutV, stepInV, stepOtherV,
-  stepHas, stepHasId, stepHasLabel, stepHasNot,
+  stepHas, stepHasId, stepHasLabel, stepHasNot, stepIs,
   stepId, stepLabel, stepValue, stepKey, stepValues, stepValueMap, stepElementMap,
-  stepPath, stepRepeat,
+  stepPath, stepRepeat, stepLoops,
   stepLimit, stepRange, stepSkip, stepTail, stepOrder,
   stepAddV, stepAddEWithModulators, stepProperty,
 } from './steps.js';
@@ -183,9 +183,19 @@ export class LocalExecutor {
 
   /** Builds the ExecutionContext handed to parent steps for running their child pipelines. */
   private makeContext(graph: Graph, trackPaths: boolean): ExecutionContext {
+    return this.buildContext(graph, trackPaths, 0);
+  }
+
+  /**
+   * Builds a context at a specific loops() value. withLoops() returns a fresh context rather
+   * than mutating, so a nested repeat() can set its own loop count without clobbering the
+   * outer loop's — each level's loops() reads the value baked into its own context object.
+   */
+  private buildContext(graph: Graph, trackPaths: boolean, loops: number): ExecutionContext {
     const ctx: ExecutionContext = {
       graph,
       trackPaths,
+      loops,
       runBranch: (child, src) => {
         let stream: Iterable<StreamItem> = src;
         for (const step of child) stream = this.applyStep(step, stream, ctx);
@@ -193,17 +203,18 @@ export class LocalExecutor {
       },
       runProject: (child, object) => {
         // child is a single value-extraction step (validated during folding).
-        const probe: ExecutionContext = { ...ctx, trackPaths: false };
+        const probe = this.buildContext(graph, false, loops);
         for (const out of this.applyStep(child[0], [wrap(object, [], false)], probe)) {
           return getValue(out, false);
         }
         return NON_PRODUCTIVE; // non-productive by(Traversal) filters the path traverser
       },
       runRooted: (child) => {
-        const probe: ExecutionContext = { ...ctx, trackPaths: false };
+        const probe = this.buildContext(graph, false, loops);
         for (const item of this.buildChain(child, probe)) return item;
         return null;
       },
+      withLoops: (n) => this.buildContext(graph, trackPaths, n),
     };
     return ctx;
   }
@@ -329,9 +340,10 @@ function requireTraversalCondition(stepName: string, arg: Arg | undefined): Pipe
 }
 
 // ── Parent step registry ────────────────────────────────────────────────────────
-// Steps that run nested child pipelines. They receive the ExecutionContext and use
-// its runBranch/runProject/runRooted helpers, so adding a new branching step is a
-// registry entry rather than a special case in applyStep.
+// Steps that need the ExecutionContext — to run nested child pipelines (path, addE,
+// repeat) or to read loop state (loops). They use its runBranch/runProject/runRooted
+// helpers and loops field, so adding such a step is a registry entry rather than a
+// special case in applyStep.
 
 const PARENT_STEPS = new Map<string, ParentStepFn>([
   ['path', (source, args, ctx) => stepPath(source, args, ctx.trackPaths, (sub, obj) => ctx.runProject(sub, obj))],
@@ -345,6 +357,7 @@ const PARENT_STEPS = new Map<string, ParentStepFn>([
     (sub) => ctx.runRooted(sub as unknown as Pipeline),
   )],
   ['repeat', (source, args, ctx) => stepRepeat(source, args[0] as unknown as RepeatSpec, ctx)],
+  ['loops', (source, args, ctx) => stepLoops(source, args, ctx)],
 ]);
 
 // ── Result copying ────────────────────────────────────────────────────────────
@@ -426,6 +439,7 @@ const STEP_REGISTRY = new Map<string, StepFn>([
   ['hasId',       stepHasId],
   ['hasLabel',    stepHasLabel],
   ['hasNot',      stepHasNot],
+  ['is',          stepIs],
   ['id',          stepId],
   ['label',       stepLabel],
   ['value',       stepValue],
diff --git a/gremlin-js/gremlin-javascript/lib/process/local/passes.ts b/gremlin-js/gremlin-javascript/lib/process/local/passes.ts
index be4088837b..948d3915b2 100644
--- a/gremlin-js/gremlin-javascript/lib/process/local/passes.ts
+++ b/gremlin-js/gremlin-javascript/lib/process/local/passes.ts
@@ -26,12 +26,12 @@ export type OptimizationPass = (pipeline: Pipeline) => Pipeline;
 const SUPPORTED_STEPS = new Set<string>([
   'V', 'E',
   'out', 'in', 'both', 'outE', 'inE', 'bothE', 'outV', 'inV', 'otherV',
-  'has', 'hasId', 'hasLabel', 'hasNot',
+  'has', 'hasId', 'hasLabel', 'hasNot', 'is',
   'id', 'label', 'values', 'valueMap', 'elementMap', 'value', 'key',
   'path',
   'limit', 'range', 'skip', 'tail',
   'order', 'by',
-  'repeat', 'times', 'until', 'emit',
+  'repeat', 'times', 'until', 'emit', 'loops',
   'addV', 'addE', 'property',
   'from', 'to',
 ]);
diff --git a/gremlin-js/gremlin-javascript/lib/process/local/steps.ts b/gremlin-js/gremlin-javascript/lib/process/local/steps.ts
index 164accf753..70a3598e2c 100644
--- a/gremlin-js/gremlin-javascript/lib/process/local/steps.ts
+++ b/gremlin-js/gremlin-javascript/lib/process/local/steps.ts
@@ -329,6 +329,20 @@ export function* stepHasNot(
   }
 }
 
+export function* stepIs(
+  source: Iterable<StreamItem>, args: Arg[], graph: Graph, trackPaths: boolean,
+): Generator<StreamItem> {
+  const arg = args[0];
+  for (const item of source) {
+    const v = getValue(item, trackPaths);
+    if (arg instanceof P || arg instanceof TextP) {
+      if (evaluatePredicate(arg, v)) yield item;
+    } else if (v === arg) {
+      yield item;
+    }
+  }
+}
+
 export function* stepHasKey(
   source: Iterable<StreamItem>, args: Arg[], graph: Graph, trackPaths: boolean,
 ): Generator<StreamItem> {
@@ -767,6 +781,24 @@ export function* stepProperty(
   }
 }
 
+// ── Loops step ────────────────────────────────────────────────────────────────
+
+/**
+ * Replaces each traverser's value with the loop count of the nearest enclosing repeat()
+ * (ctx.loops), typically tested by a following is(), e.g. until(__.loops().is(2)). Zero
+ * outside any repeat(). The loops("label") form is rejected — Tiny Gremlin has no step labels.
+ */
+export function* stepLoops(
+  source: Iterable<StreamItem>, args: Arg[], ctx: ExecutionContext,
+): Generator<StreamItem> {
+  if (args.length > 0) {
+    throw new Error('loops("label") is not supported in Tiny Gremlin; step labels are excluded');
+  }
+  for (const item of source) {
+    yield wrap(ctx.loops, getPath(item, ctx.trackPaths), ctx.trackPaths);
+  }
+}
+
 // ── Repeat step ───────────────────────────────────────────────────────────────
 
 /**
@@ -795,47 +827,54 @@ export function* stepRepeat(
   ctx: ExecutionContext,
 ): Generator<StreamItem> {
   // until()/emit() conditions are always filter traversals here; the predicate form
-  // is rejected during folding (see requireTraversalCondition in LocalExecutor).
-  const conditionHolds = (cond: Pipeline, item: StreamItem): boolean => {
-    for (const _out of ctx.runBranch(cond, [item])) return true; // filter traversal: any output => true
+  // is rejected during folding (see requireTraversalCondition in LocalExecutor). Each is
+  // run through a context carrying the loop count it should observe via loops(), so
+  // until(__.loops().is(n)) sees the same counter that times(n) compares against.
+  const conditionHolds = (cond: Pipeline, item: StreamItem, condCtx: ExecutionContext): boolean => {
+    for (const _out of condCtx.runBranch(cond, [item])) return true; // filter traversal: any output => true
     return false;
   };
 
-  const exitBeforeBody = (item: StreamItem, loops: number): boolean => {
+  const exitBeforeBody = (item: StreamItem, loops: number, condCtx: ExecutionContext): boolean => {
     if (spec.times != null && spec.timesFirst && loops >= spec.times) return true;
-    if (spec.untilFirst && spec.until != null && conditionHolds(spec.until, item)) return true;
+    if (spec.untilFirst && spec.until != null && conditionHolds(spec.until, item, condCtx)) return true;
     return false;
   };
-  const exitAfterBody = (item: StreamItem, loops: number): boolean => {
+  const exitAfterBody = (item: StreamItem, loops: number, condCtx: ExecutionContext): boolean => {
     if (spec.times != null && !spec.timesFirst && loops >= spec.times) return true;
-    if (!spec.untilFirst && spec.until != null && conditionHolds(spec.until, item)) return true;
+    if (!spec.untilFirst && spec.until != null && conditionHolds(spec.until, item, condCtx)) return true;
     return false;
   };
-  const shouldEmit = (item: StreamItem): boolean => {
+  const shouldEmit = (item: StreamItem, condCtx: ExecutionContext): boolean => {
     if (!spec.emitPresent) return false;
     if (spec.emit == null) return true; // bare emit() — emit every traverser
-    return conditionHolds(spec.emit, item);
+    return conditionHolds(spec.emit, item, condCtx);
   };
 
   let frontier: StreamItem[] = [...source];
   let loops = 0;
   while (frontier.length > 0) {
+    // The body, the while-phase until(), and an emit-before condition all observe the
+    // current loop count; the do-while until() and emit-after condition observe the
+    // post-body count.
+    const curCtx = ctx.withLoops(loops);
     // While-phase: traversers that hit a before-body exit leave the loop now;
     // the rest (optionally emitted ahead of the body) form the body's input.
     const entering: StreamItem[] = [];
     for (const item of frontier) {
-      if (exitBeforeBody(item, loops)) { yield item; continue; }
-      if (spec.emitFirst && shouldEmit(item)) yield item;
+      if (exitBeforeBody(item, loops, curCtx)) { yield item; continue; }
+      if (spec.emitFirst && shouldEmit(item, curCtx)) yield item;
       entering.push(item);
     }
     if (entering.length === 0) break;
 
     // Run the body over the entire frontier in one pass so barriers see it all.
     const nextLoops = loops + 1;
+    const nextCtx = ctx.withLoops(nextLoops);
     const nextFrontier: StreamItem[] = [];
-    for (const out of ctx.runBranch(spec.body, entering)) {
-      if (exitAfterBody(out, nextLoops)) { yield out; continue; }
-      if (!spec.emitFirst && shouldEmit(out)) yield out;
+    for (const out of curCtx.runBranch(spec.body, entering)) {
+      if (exitAfterBody(out, nextLoops, nextCtx)) { yield out; continue; }
+      if (!spec.emitFirst && shouldEmit(out, nextCtx)) yield out;
       nextFrontier.push(out);
     }
     frontier = nextFrontier;
diff --git a/gremlin-js/gremlin-javascript/lib/process/local/types.ts b/gremlin-js/gremlin-javascript/lib/process/local/types.ts
index d1aee1bb83..9e11ff7286 100644
--- a/gremlin-js/gremlin-javascript/lib/process/local/types.ts
+++ b/gremlin-js/gremlin-javascript/lib/process/local/types.ts
@@ -67,12 +67,20 @@ export type Pipeline = StepDescriptor[];
 export interface ExecutionContext {
   graph: Graph;
   trackPaths: boolean;
+  /**
+   * The loop count of the nearest enclosing repeat(), read by loops(). Zero outside any
+   * repeat() and on a repeat()'s first body pass. repeat() supplies the value to its body
+   * and its until()/emit() conditions via withLoops().
+   */
+  loops: number;
   /** Apply a child pipeline as mid-traversal steps over an existing stream (flatMap per traverser). */
   runBranch(child: Pipeline, source: Iterable<any>): Iterable<any>;
   /** Run a single value-extraction child against one object, returning its first result or NON_PRODUCTIVE. */
   runProject(child: Pipeline, object: any): any;
   /** Run a child as a complete source-rooted pipeline (e.g. addE from/to), returning the first result. */
   runRooted(child: Pipeline): any;
+  /** Derive a context with a different loops() value, used by repeat() per loop iteration. */
+  withLoops(loops: number): ExecutionContext;
 }
 
 /**
diff --git a/gremlin-js/gremlin-javascript/test/unit/tiny-gremlin-is-loops-test.js b/gremlin-js/gremlin-javascript/test/unit/tiny-gremlin-is-loops-test.js
new file mode 100644
index 0000000000..8a786ab6d5
--- /dev/null
+++ b/gremlin-js/gremlin-javascript/test/unit/tiny-gremlin-is-loops-test.js
@@ -0,0 +1,94 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+
+import { assert } from 'chai';
+import anon from '../../lib/process/anonymous-traversal.js';
+import { statics as __ } from '../../lib/process/graph-traversal.js';
+import { P } from '../../lib/process/traversal.js';
+import { buildModernGraph } from '../cucumber/local-fixtures.js';
+
+/**
+ * Unit coverage for is() (value/predicate filter) and loops() (the enclosing repeat()'s
+ * loop counter) in the Tiny Gremlin local executor. loops() is exercised through repeat()
+ * since that is where it carries meaning; the two steps are paired because loops() is inert
+ * without a following is() to test the count.
+ */
+describe('Tiny Gremlin is() and loops()', () => {
+  let g;
+  const markoId = 1;
+
+  beforeEach(() => {
+    g = anon.traversal().with_(buildModernGraph().graph);
+  });
+
+  // ── is() ──────────────────────────────────────────────────────────────────
+
+  it('is(value) keeps only equal values', async () => {
+    const ages = await g.V().values('age').is(32).toList();
+    assert.deepEqual(ages, [32]);
+  });
+
+  it('is(P) filters by predicate', async () => {
+    const ages = await g.V().values('age').is(P.lte(30)).toList();
+    assert.sameMembers(ages, [27, 29]);
+  });
+
+  it('chains is() predicates as a conjunction', async () => {
+    const ages = await g.V().values('age').is(P.gte(29)).is(P.lt(34)).toList();
+    assert.sameMembers(ages, [29, 32]);
+  });
+
+  // ── loops() ───────────────────────────────────────────────────────────────
+
+  it('until(loops().is(n)) before repeat() is while-bounded like times(n)', async () => {
+    // condition tested before the body: exits once the current loop count reaches 2
+    const names = await g.V().until(__.loops().is(2)).repeat(__.out()).values('name').toList();
+    assert.sameMembers(names, ['lop', 'ripple']);
+  });
+
+  it('until(loops().is(n)) after repeat() is do-while and matches times(n)', async () => {
+    const viaLoops = await g.V(markoId).repeat(__.out()).until(__.loops().is(2)).values('name').toList();
+    const viaTimes = await g.V(markoId).repeat(__.out()).times(2).values('name').toList();
+    assert.sameMembers(viaLoops, viaTimes);
+    assert.sameMembers(viaLoops, ['lop', 'ripple']);
+  });
+
+  it('reports the inner loop count inside a nested repeat()', async () => {
+    // inner repeat exits when ITS own loops() reaches 1 (one out() hop), then the outer
+    // repeat takes a second hop; loops() resolves to the nearest enclosing repeat().
+    const names = await g.V(markoId)
+      .repeat(__.repeat(__.out()).until(__.loops().is(1))).until(__.loops().is(2))
+      .values('name').toList();
+    assert.sameMembers(names, ['lop', 'ripple']);
+  });
+
+  it('is zero outside any repeat()', async () => {
+    const loops = await g.V().loops().toList();
+    assert.deepEqual(loops, [0, 0, 0, 0, 0, 0]);
+  });
+
+  it('rejects the loops("label") reference (no step labels in Tiny Gremlin)', async () => {
+    try {
+      await g.V(markoId).repeat(__.out()).until(__.loops('a').is(2)).toList();
+      assert.fail('expected a rejection for loops("label")');
+    } catch (err) {
+      assert.match(err.message, /loops\("label"\) is not supported/);
+    }
+  });
+});
diff --git a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/branch/Repeat.feature b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/branch/Repeat.feature
index a5652a312b..6364afefa3 100644
--- a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/branch/Repeat.feature
+++ b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/branch/Repeat.feature
@@ -951,6 +951,7 @@ Feature: Step - repeat()
       | josh |
       | peter |
 
+  @TinyGremlin
   Scenario: g_V_untilXloops_isX2XX_repeatXout_order_byXnameXX_valuesXnameX
     Given the modern graph
     And the traversal of
diff --git a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/filter/Is.feature b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/filter/Is.feature
index 93afd854b9..95e556cc9b 100644
--- a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/filter/Is.feature
+++ b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/filter/Is.feature
@@ -18,6 +18,7 @@
 @StepClassFilter @StepIs
 Feature: Step - is()
 
+  @TinyGremlin
   Scenario: g_V_valuesXageX_isX32X
     Given the modern graph
     And the traversal of
@@ -29,6 +30,7 @@ Feature: Step - is()
       | result |
       | d[32].i |
 
+  @TinyGremlin
   Scenario: g_V_valuesXageX_isX32varX
     Given the modern graph
     And using the parameter xx1 defined as "d[32].i"
@@ -41,6 +43,7 @@ Feature: Step - is()
       | result |
       | d[32].i |
 
+  @TinyGremlin
   Scenario: g_V_valuesXageX_isXlte_30X
     Given the modern graph
     And the traversal of
@@ -53,6 +56,7 @@ Feature: Step - is()
       | d[27].i |
       | d[29].i |
 
+  @TinyGremlin
   Scenario: g_V_valuesXageX_isXlte_30varX
     Given the modern graph
     And using the parameter xx1 defined as "d[30].i"
@@ -66,6 +70,7 @@ Feature: Step - is()
       | d[27].i |
       | d[29].i |
 
+  @TinyGremlin
   Scenario: g_V_valuesXageX_isXgte_29X_isXlt_34X
     Given the modern graph
     And the traversal of
@@ -78,6 +83,7 @@ Feature: Step - is()
       | d[29].i |
       | d[32].i |
 
+  @TinyGremlin
   Scenario: g_V_valuesXageX_isXgte_29vaarX_isXlt_34varX
     Given the modern graph
     And using the parameter xx1 defined as "d[29].i"