Repository: graphprotocol/graph-tooling
Pull request #1994 (2025-03-31 19:18:11)

Require immutable flag on entities
This PR adds a new validation for GraphQL schemas that requires all entities to be explicitly marked as immutable or mutable. This is the first step in transitioning subgraph developers to using immutable entities by default.

As a result, the codegen command will fail if there are any entities without an immutable flag. The error message also provides a hint on what to update and how.

In addition, if GraphQL schemas do not use immutable entities _(all entities are mutable)_, the codegen command will print a warning suggesting that immutable entities should be considered and a link to best practices.

Finally, all example subgraphs have been updated with explicit immutable flags.

### Todos
- [x] Confirm the warning message with @alex-pakalniskis

changeset-bot[bot]: ###  🦋  Changeset detected

Latest commit: 37a832665e46f65fa2f1d9e4510565d03a6b2a4a

**The changes in this PR will be included in the next version bump.**

<details><summary>This PR includes changesets to release 1 package</summary>

  | Name                     | Type  |
| ------------------------ | ----- |
| @graphprotocol/graph-cli | Minor |

</details>

Not sure what this means? [Click here  to learn what changesets are](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add another changeset to this PR](https://github.com/graphprotocol/graph-tooling/new/ion/require-immutable-flag-on-entities?filename=.changeset/sharp-parents-design.md&value=---%0A%22%40graphprotocol%2Fgraph-cli%22%3A%20patch%0A%22example-arweave-blocks-transactions%22%3A%20patch%0A%22example-cosmos-block-filtering%22%3A%20patch%0A%22example-cosmos-osmosis-token-swaps-%22%3A%20patch%0A%22example-cosmos-validator-delegations%22%3A%20patch%0A%22example-cosmos-validator-rewards%22%3A%20patch%0A%22example-ethereum-basic-event-handlers%22%3A%20patch%0A%22example-ethereum-gravatar%22%3A%20patch%0A%22example-event-handler%22%3A%20patch%0A%22example-near-blocks%22%3A%20patch%0A%22example-near-receipts%22%3A%20patch%0A%22example-substreams-powered-subgraph%22%3A%20patch%0A---%0A%0ARequire%20immutable%20flag%20on%20entities%0A)


github-actions[bot]: <!-- changesetsSnapshotPrCommentKey -->
### 🚀 Snapshot Release (`alpha`)

The latest changes of this PR are available as `alpha` on npm (based on the declared `changesets`):
| Package | Version | Info |
|------|---------|----|
| `@graphprotocol/graph-cli` | `0.97.0-alpha-20250331191845-37a832665e46f65fa2f1d9e4510565d03a6b2a4a` | [npm ↗︎](https://www.npmjs.com/package/@graphprotocol/graph-cli/v/0.97.0-alpha-20250331191845-37a832665e46f65fa2f1d9e4510565d03a6b2a4a) [unpkg ↗︎](https://unpkg.com/browse/@graphprotocol/graph-cli@0.97.0-alpha-20250331191845-37a832665e46f65fa2f1d9e4510565d03a6b2a4a/) |
matthewdarwin: cosmos is no longer supported, we should consider removing those examples instead of updating them.

Patch:
diff --git a/.changeset/tangy-pants-buy.md b/.changeset/tangy-pants-buy.md
new file mode 100644
index 00000000..f8af21c0
--- /dev/null
+++ b/.changeset/tangy-pants-buy.md
@@ -0,0 +1,5 @@
+---
+'@graphprotocol/graph-cli': minor
+---
+
+require immutable flag on entities
diff --git a/examples/arweave-blocks-transactions/schema.graphql b/examples/arweave-blocks-transactions/schema.graphql
index 3c8843df..85342ac5 100644
--- a/examples/arweave-blocks-transactions/schema.graphql
+++ b/examples/arweave-blocks-transactions/schema.graphql
@@ -1,4 +1,4 @@
-type Block @entity {
+type Block @entity(immutable: true) {
   id: ID!
 
   timestamp: BigInt!
@@ -22,7 +22,7 @@ type Block @entity {
   poa: Poa
 }
 
-type Transaction @entity {
+type Transaction @entity(immutable: true) {
   id: ID!
 
   block: Block!
@@ -39,7 +39,7 @@ type Transaction @entity {
   reward: Bytes!
 }
 
-type Poa @entity {
+type Poa @entity(immutable: true) {
   id: ID!
 
   option: String!
@@ -48,7 +48,7 @@ type Poa @entity {
   chunk: Bytes!
 }
 
-type Tag @entity {
+type Tag @entity(immutable: true) {
   id: ID!
 
   name: Bytes!
diff --git a/examples/cosmos-block-filtering/schema.graphql b/examples/cosmos-block-filtering/schema.graphql
index 803f5c7e..c52d5b4f 100644
--- a/examples/cosmos-block-filtering/schema.graphql
+++ b/examples/cosmos-block-filtering/schema.graphql
@@ -1,4 +1,4 @@
-type Block @entity {
+type Block @entity(immutable: true) {
   id: ID!
   number: BigInt
   timestamp: BigInt
diff --git a/examples/cosmos-osmosis-token-swaps/schema.graphql b/examples/cosmos-osmosis-token-swaps/schema.graphql
index baffd4b0..c6f24ecd 100644
--- a/examples/cosmos-osmosis-token-swaps/schema.graphql
+++ b/examples/cosmos-osmosis-token-swaps/schema.graphql
@@ -1,10 +1,10 @@
-type Token @entity {
+type Token @entity(immutable: true) {
   id: ID!
   amount: String
   denom: String
 }
 
-type TokenSwap @entity {
+type TokenSwap @entity(immutable: true) {
   id: ID!
   sender: String
   poolId: String
diff --git a/examples/cosmos-validator-delegations/schema.graphql b/examples/cosmos-validator-delegations/schema.graphql
index 11b1433e..a717049e 100644
--- a/examples/cosmos-validator-delegations/schema.graphql
+++ b/examples/cosmos-validator-delegations/schema.graphql
@@ -1,11 +1,11 @@
-type Delegation @entity {
+type Delegation @entity(immutable: true) {
   id: ID!
   delegatorAddress: String
   validatorAddress: String
   amount: Coin
 }
 
-type Coin @entity {
+type Coin @entity(immutable: true) {
   id: ID!
   denom: String
   amount: String
diff --git a/examples/cosmos-validator-rewards/schema.graphql b/examples/cosmos-validator-rewards/schema.graphql
index c6863ca4..6bd20b14 100644
--- a/examples/cosmos-validator-rewards/schema.graphql
+++ b/examples/cosmos-validator-rewards/schema.graphql
@@ -1,4 +1,4 @@
-type Reward @entity {
+type Reward @entity(immutable: true) {
   id: ID!
   amount: String
   validator: String
diff --git a/examples/ethereum-basic-event-handlers/schema.graphql b/examples/ethereum-basic-event-handlers/schema.graphql
index cf8774f1..928e5b91 100644
--- a/examples/ethereum-basic-event-handlers/schema.graphql
+++ b/examples/ethereum-basic-event-handlers/schema.graphql
@@ -1,11 +1,11 @@
-type NewGravatar @entity {
+type NewGravatar @entity(immutable: false) {
   id: ID!
   owner: Bytes!
   displayName: String!
   imageUrl: String!
 }
 
-type UpdatedGravatar @entity {
+type UpdatedGravatar @entity(immutable: false) {
   id: ID!
   owner: Bytes!
   displayName: String!
diff --git a/examples/ethereum-gravatar/schema.graphql b/examples/ethereum-gravatar/schema.graphql
index f4530243..e16b211a 100644
--- a/examples/ethereum-gravatar/schema.graphql
+++ b/examples/ethereum-gravatar/schema.graphql
@@ -1,4 +1,4 @@
-type Gravatar @entity {
+type Gravatar @entity(immutable: false) {
   id: ID!
   owner: Bytes!
   displayName: String!
diff --git a/examples/example-subgraph/schema.graphql b/examples/example-subgraph/schema.graphql
index f72b1bcf..ac473835 100644
--- a/examples/example-subgraph/schema.graphql
+++ b/examples/example-subgraph/schema.graphql
@@ -1,4 +1,4 @@
-type ExampleEntity @entity {
+type ExampleEntity @entity(immutable: false) {
   id: ID!
 
   optionalBoolean: Boolean
@@ -32,6 +32,6 @@ type ExampleEntity @entity {
   requiredReferenceList: [OtherEntity!]!
 }
 
-type OtherEntity @entity {
+type OtherEntity @entity(immutable: true) {
   id: ID!
 }
diff --git a/examples/near-blocks/schema.graphql b/examples/near-blocks/schema.graphql
index e72cca3e..acd43629 100644
--- a/examples/near-blocks/schema.graphql
+++ b/examples/near-blocks/schema.graphql
@@ -1,4 +1,4 @@
-type BlockEvent @entity {
+type BlockEvent @entity(immutable: true) {
   id: ID!
   number: BigInt
   hash: Bytes
diff --git a/examples/near-receipts/schema.graphql b/examples/near-receipts/schema.graphql
index b6347b28..c81680fe 100644
--- a/examples/near-receipts/schema.graphql
+++ b/examples/near-receipts/schema.graphql
@@ -1,10 +1,10 @@
-type Greeter @entity {
+type Greeter @entity(immutable: true) {
   id: ID!
   name: String!
   greetings: [Greeting!] @derivedFrom(field: "greeter")
 }
 
-type Greeting @entity {
+type Greeting @entity(immutable: true) {
   id: ID!
   greeter: Greeter!
   timestamp: BigInt!
diff --git a/examples/substreams-powered-subgraph/schema.graphql b/examples/substreams-powered-subgraph/schema.graphql
index 7b1c7d11..7f2b8008 100644
--- a/examples/substreams-powered-subgraph/schema.graphql
+++ b/examples/substreams-powered-subgraph/schema.graphql
@@ -1,4 +1,4 @@
-type Contract @entity {
+type Contract @entity(immutable: true) {
   id: ID!
 
   "The timestamp when the contract was deployed"
diff --git a/packages/cli/src/scaffold/schema.ts b/packages/cli/src/scaffold/schema.ts
index 9f66b506..ce6d8218 100644
--- a/packages/cli/src/scaffold/schema.ts
+++ b/packages/cli/src/scaffold/schema.ts
@@ -85,7 +85,7 @@ export const generateEventType = (
 
 export const generateExampleEntityType = (protocol: Protocol, events: any[]) => {
   if (protocol.hasABIs() && events.length > 0) {
-    return `type ExampleEntity @entity {
+    return `type ExampleEntity @entity(immutable: true) {
   id: Bytes!
   count: BigInt!
   ${events[0].inputs
@@ -97,7 +97,7 @@ export const generateExampleEntityType = (protocol: Protocol, events: any[]) =>
     .join('\n')}
 }`;
   }
-  return `type ExampleEntity @entity {
+  return `type ExampleEntity @entity(immutable: true) {
   id: ID!
   block: Bytes!
   count: BigInt!
diff --git a/packages/cli/src/schema.ts b/packages/cli/src/schema.ts
index 25e93a5a..9f935fe0 100644
--- a/packages/cli/src/schema.ts
+++ b/packages/cli/src/schema.ts
@@ -42,4 +42,31 @@ export default class Schema {
       )
       .map(entity => (entity as graphql.ObjectTypeDefinitionNode).name.value);
   }
+
+  immutableEntitiesCount(): number {
+    const isImmutable = (entity: graphql.ConstDirectiveNode) => {
+      return (
+        entity.arguments?.find(arg => {
+          return (
+            (arg.name.value === 'immutable' || arg.name.value === 'timeseries') &&
+            arg.value.kind === 'BooleanValue' &&
+            arg.value.value
+          );
+        }) !== undefined
+      );
+    };
+
+    return this.ast.definitions.filter(def => {
+      if (def.kind !== 'ObjectTypeDefinition') {
+        return false;
+      }
+
+      const entity = def.directives?.find(directive => directive.name.value === 'entity');
+      if (entity === undefined) {
+        return false;
+      }
+
+      return isImmutable(entity);
+    }).length;
+  }
 }
diff --git a/packages/cli/src/type-generator.ts b/packages/cli/src/type-generator.ts
index ece60762..b38d5c35 100644
--- a/packages/cli/src/type-generator.ts
+++ b/packages/cli/src/type-generator.ts
@@ -102,6 +102,14 @@ export default class TypeGenerator {
       typeGenDebug.extend('generateTypes')('Generating types for schema');
       await this.generateTypesForSchema({ schema });
 
+      if (schema.immutableEntitiesCount() === 0) {
+        toolbox.print.warning(
+          '\nThe GraphQL schema does not take advantage of using immutable entities. ' +
+            'Immutable entities can significantly improve subgraph performance.\n' +
+            'Learn more about using immutable entities: https://thegraph.com/docs/en/subgraphs/best-practices/immutable-entities-bytes-as-ids/#immutable-entities',
+        );
+      }
+
       if (this.options.subgraphSources.length > 0) {
         const ipfsClient = createIpfsClient({
           url: appendApiVersionForGraph(this.options.ipfsUrl.toString()),
diff --git a/packages/cli/src/validation/schema.ts b/packages/cli/src/validation/schema.ts
index b62cf0bd..7cfcd1af 100644
--- a/packages/cli/src/validation/schema.ts
+++ b/packages/cli/src/validation/schema.ts
@@ -1,4 +1,5 @@
 import fs from 'node:fs';
+import { print } from 'gluegun';
 import * as graphql from 'graphql/language/index.js';
 import immutable from 'immutable';
 import debugFactory from '../debug.js';
@@ -99,6 +100,36 @@ const validateEntityDirective = (def: any) => {
       ]);
 };
 
+const validateEntityDirectiveImmutableArgument = (def: graphql.ObjectTypeDefinitionNode) => {
+  validateDebugger('Validating immutable argument on entity directive for %s', def?.name?.value);
+
+  const entity = def.directives?.find(directive => directive.name.value === 'entity');
+  if (entity === undefined) {
+    return List();
+  }
+
+  const hasImmutableArg =
+    entity.arguments?.find(arg => arg.name.value === 'immutable') !== undefined;
+  const hasTimeseriesArg =
+    entity.arguments?.find(arg => arg.name.value === 'timeseries') !== undefined;
+
+  if (hasImmutableArg || hasTimeseriesArg) {
+    return List();
+  }
+
+  return immutable.fromJS([
+    {
+      loc: def.loc,
+      entity: def.name?.value,
+      message:
+        `@entity directive requires \`immutable\` argument\n  ` +
+        print.colors.yellow(
+          `Hint: Try updating the entity definition with: @entity(immutable: true)`,
+        ),
+    },
+  ]);
+};
+
 const validateEntityID = (def: any) => {
   validateDebugger('Validating entity ID for %s', def?.name?.value);
   const idField = def.fields.find((field: any) => field.name.value === 'id');
@@ -986,6 +1017,7 @@ const typeDefinitionValidators = {
       ? List.of(...validateSubgraphSchemaDirectives(def), ...validateTypeHasNoFields(def))
       : List.of(
           ...validateEntityDirective(def),
+          ...validateEntityDirectiveImmutableArgument(def),
           ...validateEntityID(def),
           ...validateEntityFields(defs, def),
           ...validateNoImportDirective(def),


Test patch:
diff --git a/packages/cli/src/scaffold/__snapshots__/cosmos.test.ts.snap b/packages/cli/src/scaffold/__snapshots__/cosmos.test.ts.snap
index ae7426d0..ad641383 100644
--- a/packages/cli/src/scaffold/__snapshots__/cosmos.test.ts.snap
+++ b/packages/cli/src/scaffold/__snapshots__/cosmos.test.ts.snap
@@ -60,7 +60,7 @@ export function handleBlock(block: cosmos.Block): void {
 `;
 
 exports[`Cosmos subgraph scaffolding > Schema (default) 1`] = `
-"type ExampleEntity @entity {
+"type ExampleEntity @entity(immutable: true) {
   id: ID!
   block: Bytes!
   count: BigInt!
diff --git a/packages/cli/src/scaffold/__snapshots__/ethereum.test.ts.snap b/packages/cli/src/scaffold/__snapshots__/ethereum.test.ts.snap
index bad4cbb0..32e19c55 100644
--- a/packages/cli/src/scaffold/__snapshots__/ethereum.test.ts.snap
+++ b/packages/cli/src/scaffold/__snapshots__/ethereum.test.ts.snap
@@ -227,7 +227,7 @@ export function handleTupleArrayEvent(event: TupleArrayEvent): void {}
 `;
 
 exports[`Ethereum subgraph scaffolding > Schema (default) 1`] = `
-"type ExampleEntity @entity {
+"type ExampleEntity @entity(immutable: true) {
   id: Bytes!
   count: BigInt!
   a: BigInt! # uint256
diff --git a/packages/cli/src/scaffold/__snapshots__/near.test.ts.snap b/packages/cli/src/scaffold/__snapshots__/near.test.ts.snap
index ff2ef932..b4aec1b2 100644
--- a/packages/cli/src/scaffold/__snapshots__/near.test.ts.snap
+++ b/packages/cli/src/scaffold/__snapshots__/near.test.ts.snap
@@ -62,7 +62,7 @@ export function handleReceipt(
 `;
 
 exports[`NEAR subgraph scaffolding > Schema (default) 1`] = `
-"type ExampleEntity @entity {
+"type ExampleEntity @entity(immutable: true) {
   id: ID!
   block: Bytes!
   count: BigInt!
diff --git a/packages/cli/tests/cli/__snapshots__/validation.test.ts.snap b/packages/cli/tests/cli/__snapshots__/validation.test.ts.snap
index 2fbee61b..ea81066d 100644
--- a/packages/cli/tests/cli/__snapshots__/validation.test.ts.snap
+++ b/packages/cli/tests/cli/__snapshots__/validation.test.ts.snap
@@ -932,6 +932,20 @@ Types generated successfully
 "
 `;
 
+exports[`Validation > Should require immutable argument on entity directive 1`] = `
+"- Load subgraph from subgraph.yaml
+✖ Failed to load subgraph from subgraph.yaml: Error in schema.graphql:
+
+  EntityA:
+  - @entity directive requires \`immutable\` argument
+    Hint: Try updating the entity definition with: @entity(immutable: true)
+"
+`;
+
+exports[`Validation > Should require immutable argument on entity directive 2`] = `1`;
+
+exports[`Validation > Should require immutable argument on entity directive 3`] = `""`;
+
 exports[`Validation > Source without address is valid 1`] = `
 "- Load subgraph from subgraph.yaml
 ✔ Load subgraph from subgraph.yaml
diff --git a/packages/cli/tests/cli/add/subgraph/schema.graphql b/packages/cli/tests/cli/add/subgraph/schema.graphql
index cf8774f1..b8fe962e 100644
--- a/packages/cli/tests/cli/add/subgraph/schema.graphql
+++ b/packages/cli/tests/cli/add/subgraph/schema.graphql
@@ -1,11 +1,11 @@
-type NewGravatar @entity {
+type NewGravatar @entity(immutable: true) {
   id: ID!
   owner: Bytes!
   displayName: String!
   imageUrl: String!
 }
 
-type UpdatedGravatar @entity {
+type UpdatedGravatar @entity(immutable: true) {
   id: ID!
   owner: Bytes!
   displayName: String!
diff --git a/packages/cli/tests/cli/validation.test.ts b/packages/cli/tests/cli/validation.test.ts
index 81a7838f..aeb90211 100644
--- a/packages/cli/tests/cli/validation.test.ts
+++ b/packages/cli/tests/cli/validation.test.ts
@@ -247,4 +247,13 @@ describe('Validation', { concurrent: true, timeout: 60_000 }, () => {
       exitCode: 0,
     },
   );
+
+  cliTest(
+    'Should require immutable argument on entity directive',
+    ['codegen', '--skip-migrations'],
+    'validation/require-immutable-argument',
+    {
+      exitCode: 1,
+    },
+  );
 });
diff --git a/packages/cli/tests/cli/validation/2d-array-is-valid/schema.graphql b/packages/cli/tests/cli/validation/2d-array-is-valid/schema.graphql
index 37e62eb4..91dad2a1 100644
--- a/packages/cli/tests/cli/validation/2d-array-is-valid/schema.graphql
+++ b/packages/cli/tests/cli/validation/2d-array-is-valid/schema.graphql
@@ -1,4 +1,4 @@
-type MyEntity @entity {
+type MyEntity @entity(immutable: true) {
   id: ID!
   childTokenURIs: [[String!]!]! # string[][]
 }
diff --git a/packages/cli/tests/cli/validation/3d-array-is-valid/schema.graphql b/packages/cli/tests/cli/validation/3d-array-is-valid/schema.graphql
index bd0b7608..6b4e8719 100644
--- a/packages/cli/tests/cli/validation/3d-array-is-valid/schema.graphql
+++ b/packages/cli/tests/cli/validation/3d-array-is-valid/schema.graphql
@@ -1,4 +1,4 @@
-type MyEntity @entity {
+type MyEntity @entity(immutable: true) {
   id: ID!
   childTokenURIs: [[[String!]!]!]! # string[][][]
 }
diff --git a/packages/cli/tests/cli/validation/abi-not-found/schema.graphql b/packages/cli/tests/cli/validation/abi-not-found/schema.graphql
index 2fa61d54..d5a41952 100644
--- a/packages/cli/tests/cli/validation/abi-not-found/schema.graphql
+++ b/packages/cli/tests/cli/validation/abi-not-found/schema.graphql
@@ -1,3 +1,3 @@
-type MyEntity @entity {
+type MyEntity @entity(immutable: true) {
   id: ID!
 }
diff --git a/packages/cli/tests/cli/validation/big-decimal-is-valid/schema.graphql b/packages/cli/tests/cli/validation/big-decimal-is-valid/schema.graphql
index 218c5b9c..1edfc3c6 100644
--- a/packages/cli/tests/cli/validation/big-decimal-is-valid/schema.graphql
+++ b/packages/cli/tests/cli/validation/big-decimal-is-valid/schema.graphql
@@ -1,4 +1,4 @@
-type MyEntity @entity {
+type MyEntity @entity(immutable: true) {
   id: ID!
   x: BigDecimal!
 }
diff --git a/packages/cli/tests/cli/validation/block-handler-filters/schema.graphql b/packages/cli/tests/cli/validation/block-handler-filters/schema.graphql
index 79d10a40..98b9b177 100644
--- a/packages/cli/tests/cli/validation/block-handler-filters/schema.graphql
+++ b/packages/cli/tests/cli/validation/block-handler-filters/schema.graphql
@@ -1,3 +1,3 @@
-type A @entity {
+type A @entity(immutable: true) {
   id: ID!
-}
\ No newline at end of file
+}
diff --git a/packages/cli/tests/cli/validation/call-function-not-found/schema.graphql b/packages/cli/tests/cli/validation/call-function-not-found/schema.graphql
index 2fa61d54..d5a41952 100644
--- a/packages/cli/tests/cli/validation/call-function-not-found/schema.graphql
+++ b/packages/cli/tests/cli/validation/call-function-not-found/schema.graphql
@@ -1,3 +1,3 @@
-type MyEntity @entity {
+type MyEntity @entity(immutable: true) {
   id: ID!
 }
diff --git a/packages/cli/tests/cli/validation/call-handler-with-tuple/schema.graphql b/packages/cli/tests/cli/validation/call-handler-with-tuple/schema.graphql
index 2fa61d54..d5a41952 100644
--- a/packages/cli/tests/cli/validation/call-handler-with-tuple/schema.graphql
+++ b/packages/cli/tests/cli/validation/call-handler-with-tuple/schema.graphql
@@ -1,3 +1,3 @@
-type MyEntity @entity {
+type MyEntity @entity(immutable: true) {
   id: ID!
 }
diff --git a/packages/cli/tests/cli/validation/conflicting-network-names/schema.graphql b/packages/cli/tests/cli/validation/conflicting-network-names/schema.graphql
index 2fa61d54..d5a41952 100644
--- a/packages/cli/tests/cli/validation/conflicting-network-names/schema.graphql
+++ b/packages/cli/tests/cli/validation/conflicting-network-names/schema.graphql
@@ -1,3 +1,3 @@
-type MyEntity @entity {
+type MyEntity @entity(immutable: true) {
   id: ID!
 }
diff --git a/packages/cli/tests/cli/validation/conflicting-protocol-names/schema.graphql b/packages/cli/tests/cli/validation/conflicting-protocol-names/schema.graphql
index 2fa61d54..d5a41952 100644
--- a/packages/cli/tests/cli/validation/conflicting-protocol-names/schema.graphql
+++ b/packages/cli/tests/cli/validation/conflicting-protocol-names/schema.graphql
@@ -1,3 +1,3 @@
-type MyEntity @entity {
+type MyEntity @entity(immutable: true) {
   id: ID!
 }
diff --git a/packages/cli/tests/cli/validation/derived-from-target-type-missing/schema.graphql b/packages/cli/tests/cli/validation/derived-from-target-type-missing/schema.graphql
index c3cc44b1..d43afb1e 100644
--- a/packages/cli/tests/cli/validation/derived-from-target-type-missing/schema.graphql
+++ b/packages/cli/tests/cli/validation/derived-from-target-type-missing/schema.graphql
@@ -1,4 +1,4 @@
-type Foo @entity {
+type Foo @entity(immutable: true) {
   id: ID!
   bars: [Bars!]! @derivedFrom(field: "foo")
 }
diff --git a/packages/cli/tests/cli/validation/derived-from-with-interface/schema.graphql b/packages/cli/tests/cli/validation/derived-from-with-interface/schema.graphql
index 42268270..cfe420f3 100644
--- a/packages/cli/tests/cli/validation/derived-from-with-interface/schema.graphql
+++ b/packages/cli/tests/cli/validation/derived-from-with-interface/schema.graphql
@@ -1,4 +1,4 @@
-type Gravatar @entity {
+type Gravatar @entity(immutable: false) {
   id: ID!
   # This field points at an interface; we allow that a derived field
   # in a type that implements the interface derives from this field
@@ -12,7 +12,7 @@ interface Account {
   gravatars: [Gravatar!]! @derivedFrom(field: "owner")
 }
 
-type UserAccount implements Account @entity {
+type UserAccount implements Account @entity(immutable: true) {
   id: ID!
   name: String!
   gravatars: [Gravatar!]! @derivedFrom(field: "owner")
diff --git a/packages/cli/tests/cli/validation/duplicate-data-source-name/schema.graphql b/packages/cli/tests/cli/validation/duplicate-data-source-name/schema.graphql
index 218c5b9c..1edfc3c6 100644
--- a/packages/cli/tests/cli/validation/duplicate-data-source-name/schema.graphql
+++ b/packages/cli/tests/cli/validation/duplicate-data-source-name/schema.graphql
@@ -1,4 +1,4 @@
-type MyEntity @entity {
+type MyEntity @entity(immutable: true) {
   id: ID!
   x: BigDecimal!
 }
diff --git a/packages/cli/tests/cli/validation/duplicate-template-name/schema.graphql b/packages/cli/tests/cli/validation/duplicate-template-name/schema.graphql
index 218c5b9c..1edfc3c6 100644
--- a/packages/cli/tests/cli/validation/duplicate-template-name/schema.graphql
+++ b/packages/cli/tests/cli/validation/duplicate-template-name/schema.graphql
@@ -1,4 +1,4 @@
-type MyEntity @entity {
+type MyEntity @entity(immutable: true) {
   id: ID!
   x: BigDecimal!
 }
diff --git a/packages/cli/tests/cli/validation/entity-field-args/schema.graphql b/packages/cli/tests/cli/validation/entity-field-args/schema.graphql
index 31a2be44..7fc28009 100644
--- a/packages/cli/tests/cli/validation/entity-field-args/schema.graphql
+++ b/packages/cli/tests/cli/validation/entity-field-args/schema.graphql
@@ -1,4 +1,4 @@
-type MyEntity @entity {
+type MyEntity @entity(immutable: true) {
   id: ID!
   foo(bar: Int!): Int!
 }
diff --git a/packages/cli/tests/cli/validation/ethereum-contract-without-handlers/schema.graphql b/packages/cli/tests/cli/validation/ethereum-contract-without-handlers/schema.graphql
index 2fa61d54..d5a41952 100644
--- a/packages/cli/tests/cli/validation/ethereum-contract-without-handlers/schema.graphql
+++ b/packages/cli/tests/cli/validation/ethereum-contract-without-handlers/schema.graphql
@@ -1,3 +1,3 @@
-type MyEntity @entity {
+type MyEntity @entity(immutable: true) {
   id: ID!
 }
diff --git a/packages/cli/tests/cli/validation/event-not-found/schema.graphql b/packages/cli/tests/cli/validation/event-not-found/schema.graphql
index 2fa61d54..d5a41952 100644
--- a/packages/cli/tests/cli/validation/event-not-found/schema.graphql
+++ b/packages/cli/tests/cli/validation/event-not-found/schema.graphql
@@ -1,3 +1,3 @@
-type MyEntity @entity {
+type MyEntity @entity(immutable: true) {
   id: ID!
 }
diff --git a/packages/cli/tests/cli/validation/example-values-found/schema.graphql b/packages/cli/tests/cli/validation/example-values-found/schema.graphql
index 2fa61d54..d5a41952 100644
--- a/packages/cli/tests/cli/validation/example-values-found/schema.graphql
+++ b/packages/cli/tests/cli/validation/example-values-found/schema.graphql
@@ -1,3 +1,3 @@
-type MyEntity @entity {
+type MyEntity @entity(immutable: true) {
   id: ID!
 }
diff --git a/packages/cli/tests/cli/validation/indexer-hints/schema.graphql b/packages/cli/tests/cli/validation/indexer-hints/schema.graphql
index 218c5b9c..1edfc3c6 100644
--- a/packages/cli/tests/cli/validation/indexer-hints/schema.graphql
+++ b/packages/cli/tests/cli/validation/indexer-hints/schema.graphql
@@ -1,4 +1,4 @@
-type MyEntity @entity {
+type MyEntity @entity(immutable: true) {
   id: ID!
   x: BigDecimal!
 }
diff --git a/packages/cli/tests/cli/validation/invalid-abis/schema.graphql b/packages/cli/tests/cli/validation/invalid-abis/schema.graphql
index 2fa61d54..d5a41952 100644
--- a/packages/cli/tests/cli/validation/invalid-abis/schema.graphql
+++ b/packages/cli/tests/cli/validation/invalid-abis/schema.graphql
@@ -1,3 +1,3 @@
-type MyEntity @entity {
+type MyEntity @entity(immutable: true) {
   id: ID!
 }
diff --git a/packages/cli/tests/cli/validation/invalid-contract-addresses/schema.graphql b/packages/cli/tests/cli/validation/invalid-contract-addresses/schema.graphql
index 2fa61d54..d5a41952 100644
--- a/packages/cli/tests/cli/validation/invalid-contract-addresses/schema.graphql
+++ b/packages/cli/tests/cli/validation/invalid-contract-addresses/schema.graphql
@@ -1,3 +1,3 @@
-type MyEntity @entity {
+type MyEntity @entity(immutable: true) {
   id: ID!
 }
diff --git a/packages/cli/tests/cli/validation/invalid-data-source-template/schema.graphql b/packages/cli/tests/cli/validation/invalid-data-source-template/schema.graphql
index 2fa61d54..d5a41952 100644
--- a/packages/cli/tests/cli/validation/invalid-data-source-template/schema.graphql
+++ b/packages/cli/tests/cli/validation/invalid-data-source-template/schema.graphql
@@ -1,3 +1,3 @@
-type MyEntity @entity {
+type MyEntity @entity(immutable: true) {
   id: ID!
 }
diff --git a/packages/cli/tests/cli/validation/invalid-entity-field-types/schema.graphql b/packages/cli/tests/cli/validation/invalid-entity-field-types/schema.graphql
index 7bd510a0..e0939648 100644
--- a/packages/cli/tests/cli/validation/invalid-entity-field-types/schema.graphql
+++ b/packages/cli/tests/cli/validation/invalid-entity-field-types/schema.graphql
@@ -1,4 +1,4 @@
-type MyEntity @entity {
+type MyEntity @entity(immutable: true) {
   id: ID!
 
   isSet1: bool
diff --git a/packages/cli/tests/cli/validation/invalid-fulltext-directive/schema.graphql b/packages/cli/tests/cli/validation/invalid-fulltext-directive/schema.graphql
index e9f26c94..96b6b613 100644
--- a/packages/cli/tests/cli/validation/invalid-fulltext-directive/schema.graphql
+++ b/packages/cli/tests/cli/validation/invalid-fulltext-directive/schema.graphql
@@ -117,7 +117,7 @@ type _Schema_
     }
   ]
 )
-type A @entity @fulltext(
+type A @entity(immutable: true) @fulltext(
   name: "FulltextOnWrongType"
   language: en
   algorithm: rank
@@ -134,7 +134,7 @@ type A @entity @fulltext(
   id: ID!
 }
 
-type B @entity {
+type B @entity(immutable: true) {
   id: ID!
   foo: String!
 }
diff --git a/packages/cli/tests/cli/validation/invalid-graphql-schema/schema.graphql b/packages/cli/tests/cli/validation/invalid-graphql-schema/schema.graphql
index 33ef8f4b..350d9b09 100644
--- a/packages/cli/tests/cli/validation/invalid-graphql-schema/schema.graphql
+++ b/packages/cli/tests/cli/validation/invalid-graphql-schema/schema.graphql
@@ -1,8 +1,8 @@
-type Something @entity {
+type Something @entity(immutable: true) {
   id: ID!
 }
 
-type MyEntity @entity {
+type MyEntity @entity(immutable: true) {
   id: ID!
   invalidField: [Something]!
 }
diff --git a/packages/cli/tests/cli/validation/missing-entity-id/schema.graphql b/packages/cli/tests/cli/validation/missing-entity-id/schema.graphql
index ed0670aa..bba9058a 100644
--- a/packages/cli/tests/cli/validation/missing-entity-id/schema.graphql
+++ b/packages/cli/tests/cli/validation/missing-entity-id/schema.graphql
@@ -1,3 +1,3 @@
-type MyEntity @entity {
+type MyEntity @entity(immutable: true) {
   name: String!
 }
diff --git a/packages/cli/tests/cli/validation/missing-or-invalid-derived-from-fields/schema.graphql b/packages/cli/tests/cli/validation/missing-or-invalid-derived-from-fields/schema.graphql
index 1894f34e..5e182fb1 100644
--- a/packages/cli/tests/cli/validation/missing-or-invalid-derived-from-fields/schema.graphql
+++ b/packages/cli/tests/cli/validation/missing-or-invalid-derived-from-fields/schema.graphql
@@ -1,4 +1,4 @@
-type A @entity {
+type A @entity(immutable: true) {
   id: ID!
   b: B @derivedFrom(field: "a")
   c: [C!]! @derivedFrom(field: "a")
@@ -9,32 +9,32 @@ type A @entity {
   h: H @derivedFrom(field: "a")
 }
 
-type B @entity {
+type B @entity(immutable: true) {
   id: ID!
 }
 
-type C @entity {
+type C @entity(immutable: true) {
   id: ID!
 }
 
-type D @entity {
+type D @entity(immutable: true) {
   id: ID!
 }
 
-type E @entity {
+type E @entity(immutable: true) {
   id: ID!
 }
 
-type F @entity {
+type F @entity(immutable: true) {
   id: ID!
 }
 
-type G @entity {
+type G @entity(immutable: true) {
   id: ID!
   a: BigInt
 }
 
-type H @entity {
+type H @entity(immutable: true) {
   id: ID!
   a: A!
 }
diff --git a/packages/cli/tests/cli/validation/near-is-valid/schema.graphql b/packages/cli/tests/cli/validation/near-is-valid/schema.graphql
index ddf2095e..c15de835 100644
--- a/packages/cli/tests/cli/validation/near-is-valid/schema.graphql
+++ b/packages/cli/tests/cli/validation/near-is-valid/schema.graphql
@@ -1,8 +1,8 @@
-type MyEntity @entity {
+type MyEntity @entity(immutable: true) {
   id: ID!
   x: Account!
 }
 
-type Account @entity {
+type Account @entity(immutable: true) {
   id: ID!
 }
diff --git a/packages/cli/tests/cli/validation/nested-template-nice-error/schema.graphql b/packages/cli/tests/cli/validation/nested-template-nice-error/schema.graphql
index 2fa61d54..d5a41952 100644
--- a/packages/cli/tests/cli/validation/nested-template-nice-error/schema.graphql
+++ b/packages/cli/tests/cli/validation/nested-template-nice-error/schema.graphql
@@ -1,3 +1,3 @@
-type MyEntity @entity {
+type MyEntity @entity(immutable: true) {
   id: ID!
 }
diff --git a/packages/cli/tests/cli/validation/no-network-names/schema.graphql b/packages/cli/tests/cli/validation/no-network-names/schema.graphql
index 2fa61d54..d5a41952 100644
--- a/packages/cli/tests/cli/validation/no-network-names/schema.graphql
+++ b/packages/cli/tests/cli/validation/no-network-names/schema.graphql
@@ -1,3 +1,3 @@
-type MyEntity @entity {
+type MyEntity @entity(immutable: true) {
   id: ID!
 }
diff --git a/packages/cli/tests/cli/validation/require-immutable-argument.stderr b/packages/cli/tests/cli/validation/require-immutable-argument.stderr
new file mode 100644
index 00000000..a0896bef
--- /dev/null
+++ b/packages/cli/tests/cli/validation/require-immutable-argument.stderr
@@ -0,0 +1,6 @@
+- Load subgraph from subgraph.yaml
+✖ Failed to load subgraph from subgraph.yaml: Error in schema.graphql:
+
+  EntityA:
+  - @entity directive requires `immutable` argument
+    Hint: Try updating the entity definition with: @entity(immutable: true)
diff --git a/packages/cli/tests/cli/validation/require-immutable-argument/Abi.json b/packages/cli/tests/cli/validation/require-immutable-argument/Abi.json
new file mode 100644
index 00000000..4d05f583
--- /dev/null
+++ b/packages/cli/tests/cli/validation/require-immutable-argument/Abi.json
@@ -0,0 +1,7 @@
+[
+  {
+    "type": "event",
+    "name": "ExampleEvent",
+    "inputs": [{ "type": "string" }]
+  }
+]
diff --git a/packages/cli/tests/cli/validation/require-immutable-argument/mapping.ts b/packages/cli/tests/cli/validation/require-immutable-argument/mapping.ts
new file mode 100644
index 00000000..e69de29b
diff --git a/packages/cli/tests/cli/validation/require-immutable-argument/schema.graphql b/packages/cli/tests/cli/validation/require-immutable-argument/schema.graphql
new file mode 100644
index 00000000..6934ebc2
--- /dev/null
+++ b/packages/cli/tests/cli/validation/require-immutable-argument/schema.graphql
@@ -0,0 +1,19 @@
+# Entity is not valid because it is missing the immutable argument;
+type EntityA @entity {
+    id: ID!
+}
+
+# Entity is valid because it has the immutable argument;
+type EntityB @entity(immutable: true) {
+    id: ID!
+}
+
+# Entity is valid because it has the immutable argument;
+type EntityC @entity(immutable: false) {
+    id: ID!
+}
+
+# Entity is valid because the timeseries argument ensures immutability;
+type EntityD @entity(timeseries: true) {
+    id: ID!
+}
diff --git a/packages/cli/tests/cli/validation/require-immutable-argument/subgraph.yaml b/packages/cli/tests/cli/validation/require-immutable-argument/subgraph.yaml
new file mode 100644
index 00000000..901269f2
--- /dev/null
+++ b/packages/cli/tests/cli/validation/require-immutable-argument/subgraph.yaml
@@ -0,0 +1,25 @@
+specVersion: 0.0.1
+schema:
+  file: ./schema.graphql
+dataSources:
+  - kind: ethereum/contract
+    name: ExampleContract
+    source:
+      address: '22843e74c59580b3eaf6c233fa67d8b7c561a835'
+      abi: ExampleEvent
+    mapping:
+      kind: ethereum/events
+      apiVersion: 0.0.5
+      language: wasm/assemblyscript
+      file: ./mapping.ts
+      entities:
+        - EntityA
+        - EntityB
+        - EntityC
+        - EntityD
+      abis:
+        - name: ExampleEvent
+          file: ./Abi.json
+      eventHandlers:
+        - event: ExampleEvent(string)
+          handler: handleExampleEvent
diff --git a/packages/cli/tests/cli/validation/source-without-address-is-valid/schema.graphql b/packages/cli/tests/cli/validation/source-without-address-is-valid/schema.graphql
index 2fa61d54..d5a41952 100644
--- a/packages/cli/tests/cli/validation/source-without-address-is-valid/schema.graphql
+++ b/packages/cli/tests/cli/validation/source-without-address-is-valid/schema.graphql
@@ -1,3 +1,3 @@
-type MyEntity @entity {
+type MyEntity @entity(immutable: true) {
   id: ID!
 }
diff --git a/packages/cli/tests/cli/validation/topic0-is-valid/schema.graphql b/packages/cli/tests/cli/validation/topic0-is-valid/schema.graphql
index 218c5b9c..1edfc3c6 100644
--- a/packages/cli/tests/cli/validation/topic0-is-valid/schema.graphql
+++ b/packages/cli/tests/cli/validation/topic0-is-valid/schema.graphql
@@ -1,4 +1,4 @@
-type MyEntity @entity {
+type MyEntity @entity(immutable: true) {
   id: ID!
   x: BigDecimal!
 }