diff --git a/ivette/src/frama-c/kernel/Messages.tsx b/ivette/src/frama-c/kernel/Messages.tsx
index b3d26cfc80b4f02920fd83fdfda051e182e6f77a..d635b8fa23449d2a3661fc008177daf3b796ff1c 100644
--- a/ivette/src/frama-c/kernel/Messages.tsx
+++ b/ivette/src/frama-c/kernel/Messages.tsx
@@ -81,6 +81,7 @@ const kindFilter: KindFilter = {
    messages. They are all shown by default. */
 const pluginFilter: PluginFilter = {
   'aorai': true,
+  'cg': true,
   'dive': true,
   'e-acsl': true,
   'eva': true,
diff --git a/ivette/src/frama-c/plugins/callgraph/api/index.ts b/ivette/src/frama-c/plugins/callgraph/api/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..e231a66fbbae3bb315cd9a85a06f47096f0311db
--- /dev/null
+++ b/ivette/src/frama-c/plugins/callgraph/api/index.ts
@@ -0,0 +1,217 @@
+/* ************************************************************************ */
+/*                                                                          */
+/*   This file is part of Frama-C.                                          */
+/*                                                                          */
+/*   Copyright (C) 2007-2023                                                */
+/*     CEA (Commissariat à l'énergie atomique et aux énergies               */
+/*          alternatives)                                                   */
+/*                                                                          */
+/*   you can redistribute it and/or modify it under the terms of the GNU    */
+/*   Lesser General Public License as published by the Free Software        */
+/*   Foundation, version 2.1.                                               */
+/*                                                                          */
+/*   It is distributed in the hope that it will be useful,                  */
+/*   but WITHOUT ANY WARRANTY; without even the implied warranty of         */
+/*   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the          */
+/*   GNU Lesser General Public License for more details.                    */
+/*                                                                          */
+/*   See the GNU Lesser General Public License version 2.1                  */
+/*   for more details (enclosed in the file licenses/LGPLv2.1).             */
+/*                                                                          */
+/* ************************************************************************ */
+
+/* --- Generated Frama-C Server API --- */
+
+/**
+   Callgraph
+   @packageDocumentation
+   @module frama-c/plugins/callgraph/api
+*/
+
+//@ts-ignore
+import * as Json from 'dome/data/json';
+//@ts-ignore
+import * as Compare from 'dome/data/compare';
+//@ts-ignore
+import * as Server from 'frama-c/server';
+//@ts-ignore
+import * as State from 'frama-c/states';
+
+//@ts-ignore
+import { byFct } from 'frama-c/kernel/api/ast';
+//@ts-ignore
+import { fct } from 'frama-c/kernel/api/ast';
+//@ts-ignore
+import { fctDefault } from 'frama-c/kernel/api/ast';
+//@ts-ignore
+import { jFct } from 'frama-c/kernel/api/ast';
+//@ts-ignore
+import { byTag } from 'frama-c/kernel/api/data';
+//@ts-ignore
+import { jTag } from 'frama-c/kernel/api/data';
+//@ts-ignore
+import { tag } from 'frama-c/kernel/api/data';
+//@ts-ignore
+import { tagDefault } from 'frama-c/kernel/api/data';
+
+export interface vertex {
+  /** The function represented by the node */
+  kf: fct;
+  /** whether this node is the root of a service */
+  is_root: boolean;
+  /** the root of this node's service */
+  root: fct;
+}
+
+/** Decoder for `vertex` */
+export const jVertex: Json.Decoder<vertex> =
+  Json.jObject({ kf: jFct, is_root: Json.jBoolean, root: jFct,});
+
+/** Natural order for `vertex` */
+export const byVertex: Compare.Order<vertex> =
+  Compare.byFields
+    <{ kf: fct, is_root: boolean, root: fct }>({
+    kf: byFct,
+    is_root: Compare.boolean,
+    root: byFct,
+  });
+
+/** Default value for `vertex` */
+export const vertexDefault: vertex =
+  { kf: fctDefault, is_root: false, root: fctDefault };
+
+/** Whether the call goes through services or not */
+export enum edgeKind {
+  /** a call between two services */
+  inter_services = 'inter_services',
+  /** a call inside a service */
+  inter_functions = 'inter_functions',
+  /** both cases above */
+  both = 'both',
+}
+
+/** Decoder for `edgeKind` */
+export const jEdgeKind: Json.Decoder<edgeKind> = Json.jEnum(edgeKind);
+
+/** Natural order for `edgeKind` */
+export const byEdgeKind: Compare.Order<edgeKind> = Compare.byEnum(edgeKind);
+
+/** Default value for `edgeKind` */
+export const edgeKindDefault: edgeKind = edgeKind.inter_services;
+
+const edgeKindTags_internal: Server.GetRequest<null,tag[]> = {
+  kind: Server.RqKind.GET,
+  name:   'plugins.callgraph.edgeKindTags',
+  input:  Json.jNull,
+  output: Json.jArray(jTag),
+  signals: [],
+};
+/** Registered tags for the above type. */
+export const edgeKindTags: Server.GetRequest<null,tag[]>= edgeKindTags_internal;
+
+export interface edge {
+  /** src */
+  src: fct;
+  /** dst */
+  dst: fct;
+  /** kind */
+  kind: edgeKind;
+}
+
+/** Decoder for `edge` */
+export const jEdge: Json.Decoder<edge> =
+  Json.jObject({ src: jFct, dst: jFct, kind: jEdgeKind,});
+
+/** Natural order for `edge` */
+export const byEdge: Compare.Order<edge> =
+  Compare.byFields
+    <{ src: fct, dst: fct, kind: edgeKind }>({
+    src: byFct,
+    dst: byFct,
+    kind: byEdgeKind,
+  });
+
+/** Default value for `edge` */
+export const edgeDefault: edge =
+  { src: fctDefault, dst: fctDefault, kind: edgeKindDefault };
+
+/** The callgraph of the current project */
+export interface graph {
+  /** vertices */
+  vertices: vertex[];
+  /** edges */
+  edges: edge[];
+}
+
+/** Decoder for `graph` */
+export const jGraph: Json.Decoder<graph> =
+  Json.jObject({ vertices: Json.jArray(jVertex), edges: Json.jArray(jEdge),});
+
+/** Natural order for `graph` */
+export const byGraph: Compare.Order<graph> =
+  Compare.byFields
+    <{ vertices: vertex[], edges: edge[] }>({
+    vertices: Compare.array(byVertex),
+    edges: Compare.array(byEdge),
+  });
+
+/** Default value for `graph` */
+export const graphDefault: graph = { vertices: [], edges: [] };
+
+/** Signal for state [`callgraph`](#callgraph)  */
+export const signalCallgraph: Server.Signal = {
+  name: 'plugins.callgraph.signalCallgraph',
+};
+
+const getCallgraph_internal: Server.GetRequest<null,graph | undefined> = {
+  kind: Server.RqKind.GET,
+  name:   'plugins.callgraph.getCallgraph',
+  input:  Json.jNull,
+  output: Json.jOption(jGraph),
+  signals: [],
+};
+/** Getter for state [`callgraph`](#callgraph)  */
+export const getCallgraph: Server.GetRequest<null,graph | undefined>= getCallgraph_internal;
+
+const callgraph_internal: State.Value<graph | undefined> = {
+  name: 'plugins.callgraph.callgraph',
+  signal: signalCallgraph,
+  getter: getCallgraph,
+};
+/** The current callgraph or an empty graph if it has not been computed yet */
+export const callgraph: State.Value<graph | undefined> = callgraph_internal;
+
+/** Signal for state [`isComputed`](#iscomputed)  */
+export const signalIsComputed: Server.Signal = {
+  name: 'plugins.callgraph.signalIsComputed',
+};
+
+const getIsComputed_internal: Server.GetRequest<null,boolean> = {
+  kind: Server.RqKind.GET,
+  name:   'plugins.callgraph.getIsComputed',
+  input:  Json.jNull,
+  output: Json.jBoolean,
+  signals: [],
+};
+/** Getter for state [`isComputed`](#iscomputed)  */
+export const getIsComputed: Server.GetRequest<null,boolean>= getIsComputed_internal;
+
+const isComputed_internal: State.Value<boolean> = {
+  name: 'plugins.callgraph.isComputed',
+  signal: signalIsComputed,
+  getter: getIsComputed,
+};
+/** This boolean is true if the graph has been computed */
+export const isComputed: State.Value<boolean> = isComputed_internal;
+
+const compute_internal: Server.ExecRequest<null,null> = {
+  kind: Server.RqKind.EXEC,
+  name:   'plugins.callgraph.compute',
+  input:  Json.jNull,
+  output: Json.jNull,
+  signals: [],
+};
+/** Compute the callgraph for the current project */
+export const compute: Server.ExecRequest<null,null>= compute_internal;
+
+/* ------------------------------------- */
diff --git a/ivette/src/frama-c/plugins/callgraph/callgraph.css b/ivette/src/frama-c/plugins/callgraph/callgraph.css
new file mode 100644
index 0000000000000000000000000000000000000000..4360e1461c2db03e11e4e3d205ce09e17ea74809
--- /dev/null
+++ b/ivette/src/frama-c/plugins/callgraph/callgraph.css
@@ -0,0 +1,6 @@
+.callgraph-computing {
+  max-width: 90%;
+  max-height: 200px;
+  fill: var(--info-text-discrete);
+  margin: auto;
+}
diff --git a/ivette/src/frama-c/plugins/callgraph/graph-style.json b/ivette/src/frama-c/plugins/callgraph/graph-style.json
new file mode 100644
index 0000000000000000000000000000000000000000..e1aff869e939ed8a04bbc64618ede99ce4e19db3
--- /dev/null
+++ b/ivette/src/frama-c/plugins/callgraph/graph-style.json
@@ -0,0 +1,42 @@
+[
+  {
+    "selector": "node",
+    "style": {
+      "shape": "round-rectangle",
+      "background-color": "#666",
+      "label": "data(id)",
+      "color": "white",
+      "text-outline-width": 2,
+      "text-outline-color": "#666",
+      "text-valign" : "center",
+      "padding" : "6px",
+      "border-width": 1,
+      "text-wrap" : "wrap"
+    }
+  },
+  {
+    "selector": "node[?is_root]",
+    "style": {
+      "border-width": "2px"
+    }
+  },
+  {
+    "selector": "edge",
+    "style": {
+      "width": 2,
+      "line-color": "#888",
+      "curve-style": "bezier",
+      "target-arrow-shape": "vee",
+      "target-arrow-color": "#888",
+      "arrow-scale": 2.0
+    }
+  },
+  {
+    "selector": ".callstack-selected",
+    "style": {
+      "overlay-color": "#8bf",
+      "overlay-padding": "8px",
+      "overlay-opacity": 0.4
+    }
+  }
+]
diff --git a/ivette/src/frama-c/plugins/callgraph/index.tsx b/ivette/src/frama-c/plugins/callgraph/index.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..e0b1861ef05335fefc5bed6d61b246789f732162
--- /dev/null
+++ b/ivette/src/frama-c/plugins/callgraph/index.tsx
@@ -0,0 +1,210 @@
+/* ************************************************************************ */
+/*                                                                          */
+/*   This file is part of Frama-C.                                          */
+/*                                                                          */
+/*   Copyright (C) 2007-2023                                                */
+/*     CEA (Commissariat à l'énergie atomique et aux énergies               */
+/*          alternatives)                                                   */
+/*                                                                          */
+/*   you can redistribute it and/or modify it under the terms of the GNU    */
+/*   Lesser General Public License as published by the Free Software        */
+/*   Foundation, version 2.1.                                               */
+/*                                                                          */
+/*   It is distributed in the hope that it will be useful,                  */
+/*   but WITHOUT ANY WARRANTY; without even the implied warranty of         */
+/*   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the          */
+/*   GNU Lesser General Public License for more details.                    */
+/*                                                                          */
+/*   See the GNU Lesser General Public License version 2.1                  */
+/*   for more details (enclosed in the file licenses/LGPLv2.1).             */
+/*                                                                          */
+/* ************************************************************************ */
+
+import React, { useEffect, useState } from 'react';
+import _ from 'lodash';
+import * as Ivette from 'ivette';
+import * as Server from 'frama-c/server';
+
+import * as AstAPI from 'frama-c/kernel/api/ast';
+import * as CgAPI from './api';
+import * as ValuesAPI from 'frama-c/plugins/eva/api/values';
+
+import Cy from 'cytoscape';
+import CytoscapeComponent from 'react-cytoscapejs';
+import 'frama-c/plugins/dive/cytoscape_libs';
+import 'cytoscape-panzoom/cytoscape.js-panzoom.css';
+import style from './graph-style.json';
+
+import { useGlobalState } from 'dome/data/states';
+import { useRequest, useSelection, useSyncValue } from 'frama-c/states';
+
+import gearsIcon from 'frama-c/plugins/eva/images/gears.svg';
+import { CallstackState } from 'frama-c/plugins/eva/valuetable';
+
+import './callgraph.css';
+
+
+// --------------------------------------------------------------------------
+// --- Nodes label measurement
+// --------------------------------------------------------------------------
+
+/* eslint-disable @typescript-eslint/no-explicit-any */
+function getWidth(node: any): string {
+  const padding = 10;
+  const min = 50;
+  const canvas = document.querySelector('canvas[data-id="layer2-node"]');
+  if (canvas instanceof HTMLCanvasElement) {
+    const context = canvas.getContext('2d');
+    if (context) {
+      const fStyle = node.pstyle('font-style').strValue;
+      const weight = node.pstyle('font-weight').strValue;
+      const size = node.pstyle('font-size').pfValue;
+      const family = node.pstyle('font-family').strValue;
+      context.font = `${fStyle} ${weight} ${size}px ${family}`;
+      const width = context.measureText(node.data('id')).width;
+      return `${Math.max(min, width + padding)}px`;
+    }
+  }
+  return `${min}px`;
+}
+/* eslint-enable @typescript-eslint/no-explicit-any */
+
+(style as unknown[]).push({
+    selector: 'node',
+    style: { width: getWidth }
+  });
+
+
+// --------------------------------------------------------------------------
+// --- Graph
+// --------------------------------------------------------------------------
+
+function edgeId(source: AstAPI.fct, target: AstAPI.fct): string {
+  return `${source}-${target}`;
+}
+
+function convertGraph(graph: CgAPI.graph): object[] {
+  const elements = [];
+  for (const v of graph.vertices) {
+    elements.push({ data: { ...v, id: v.kf } });
+  }
+  for (const e of graph.edges) {
+    const id = edgeId(e.src, e.dst);
+    elements.push({ data: { ...e, id, source: e.src, target: e.dst } });
+  }
+  return elements;
+}
+
+type callstack = {
+  callee: AstAPI.fct,
+  caller?: AstAPI.fct,
+  stmt?: AstAPI.marker,
+  rank?: number
+}[]
+
+function selectFct(cy: Cy.Core, fct: string | undefined): void {
+  const className = 'marker-selected';
+  cy.$(`.${className}`).removeClass(className);
+  if (fct) {
+    cy.$(`node[id='${fct}']`).addClass(className);
+  }
+}
+
+function selectCallstack(cy: Cy.Core, callstack: callstack | undefined): void {
+  const className = 'callstack-selected';
+  cy.$(`.${className}`).removeClass(className);
+  callstack?.forEach((call) => {
+    cy.$(`node[id='${call.callee}']`).addClass(className);
+    if (call.caller) {
+      const id = edgeId(call.caller, call.callee);
+      cy.$(`edge[id='${id}']`).addClass(className);
+    }
+  });
+}
+
+function Callgraph() : JSX.Element {
+  const isComputed = useSyncValue(CgAPI.isComputed);
+  const graph = useSyncValue(CgAPI.callgraph);
+  const [cy, setCy] = useState<Cy.Core>();
+  const [cs] = useGlobalState(CallstackState);
+  const [selection, setSelection] = useSelection();
+  const callstack = useRequest(ValuesAPI.getCallstackInfo, cs);
+
+  const layout = { name: 'cola', nodeSpacing: 32 };
+  const computedStyle = getComputedStyle(document.documentElement);
+  const styleVariables =
+    { ['code-select']: computedStyle.getPropertyValue("--code-select") };
+
+  const completeStyle = [
+    ...style,
+    {
+      "selector": ".marker-selected",
+      "style": { "background-color": styleVariables['code-select'] }
+    }
+  ];
+
+  // Marker selection
+  useEffect(() => {
+    cy && selectFct(cy, selection.current?.fct);
+  }, [cy, selection]);
+
+  // Callstack selection
+  useEffect(() => {
+    cy && selectCallstack(cy, callstack);
+  }, [cy, callstack]);
+
+  // Click on graph
+  useEffect(() => {
+    if (cy) {
+      cy.off('click');
+      cy.on('click', 'node', (event) => {
+        const fct = event.target.id() as string;
+        setSelection({ location: { fct } });
+      });
+    }
+  }, [cy, setSelection]);
+
+
+  if (isComputed === false) {
+    Server.send(CgAPI.compute, null);
+    return (<img src={gearsIcon} className="callgraph-computing" />);
+  }
+  else if (graph !== undefined) {
+    return (
+      <CytoscapeComponent
+        elements={convertGraph(graph)}
+        stylesheet={completeStyle}
+        cy={setCy}
+        layout={layout}
+        style={{ width: '100%', height: '100%' }}
+      />);
+  }
+  else {
+    return (<></>);
+  }
+}
+
+
+// --------------------------------------------------------------------------
+// --- Ivette Component
+// --------------------------------------------------------------------------
+
+function CallgraphComponent(): JSX.Element {
+  // Component
+  return (
+    <>
+      <Callgraph />
+    </>
+  );
+}
+
+
+Ivette.registerComponent({
+  id: 'frama-c.plugins.callgraph',
+  label: 'Call Graph',
+  group: 'frama-c.plugins',
+  rank: 3,
+  title:
+    'Display a graph showing calls between functions.',
+  children: <CallgraphComponent />,
+});
diff --git a/ivette/src/frama-c/plugins/callgraph/pkg.json b/ivette/src/frama-c/plugins/callgraph/pkg.json
new file mode 100644
index 0000000000000000000000000000000000000000..01be594c9eea276d40d1967201e4675ea3cdc1f8
--- /dev/null
+++ b/ivette/src/frama-c/plugins/callgraph/pkg.json
@@ -0,0 +1,3 @@
+{
+  "name": "Frama-C/Callgraph"
+}
diff --git a/ivette/src/frama-c/plugins/eva/valuetable.tsx b/ivette/src/frama-c/plugins/eva/valuetable.tsx
index 2b2490d51ad12a8a60b1140347a359400b40d4b9..95a83acbc7044e37a0de10da29d3254b55f1f7ab 100644
--- a/ivette/src/frama-c/plugins/eva/valuetable.tsx
+++ b/ivette/src/frama-c/plugins/eva/valuetable.tsx
@@ -972,7 +972,7 @@ function useEvaluationMode(props: EvaluationModeProps): void {
 /* -------------------------------------------------------------------------- */
 
 /* Table's state. It is global for when the user changes the view. */
-const CallstackState = new GlobalState<callstack>('Summary');
+export const CallstackState = new GlobalState<callstack>('Summary');
 const FunctionsManagerState = new GlobalState(new FunctionsManager());
 const FocusState = new GlobalState<Probe | undefined>(undefined);
 
diff --git a/src/plugins/callgraph/callgraph_api.ml b/src/plugins/callgraph/callgraph_api.ml
index 712ccd11aed0a5ecd49c658f5e3973f8574bcb6b..8ae2e7c7fca02cb0f65f6ff832b96ecad84799a3 100644
--- a/src/plugins/callgraph/callgraph_api.ml
+++ b/src/plugins/callgraph/callgraph_api.ml
@@ -43,6 +43,9 @@ module type Graph = sig
   val is_computed: unit -> bool
   (** Is the graph already built? *)
 
+  val add_hook: (G.t -> unit) -> unit
+  (** Call registered hook each time the graph is computed *)
+
   val self: State.t
 
 end
@@ -60,7 +63,6 @@ module type Services = sig
 
   val entry_point: unit -> G.V.t option
   val is_root: Kernel_function.t -> bool
-
 end
 
 (*
diff --git a/src/plugins/callgraph/cg.ml b/src/plugins/callgraph/cg.ml
index 24c12d9a2ea69474d83e03035708ec5161c69154..1d0816867404ecbe6f0b4d0135e33702d3e57f56 100644
--- a/src/plugins/callgraph/cg.ml
+++ b/src/plugins/callgraph/cg.ml
@@ -66,8 +66,11 @@ module State =
       let dependencies = [ Eva.Analysis.self; Globals.Functions.self ]
     end)
 
+module StateHook = Hook.Build (D)
+
 let self = State.self
 let is_computed () = State.is_computed ()
+let add_hook = StateHook.extend
 
 (** @return the list of functions which address is taken.*)
 let get_pointed_kfs =
@@ -101,7 +104,6 @@ let get_pointed_kfs =
     match !res with
     | None ->
       let l = compute () in
-      State.mark_as_computed ();
       res := Some l;
       l
     | Some l -> l
@@ -232,6 +234,8 @@ let compute () =
     semantic_compute g
   end else
     (if Eva.Analysis.is_computed () then semantic_compute else syntactic_compute) g;
+  State.mark_as_computed ();
+  StateHook.apply g;
   g
 
 let get () = State.memo compute
diff --git a/src/plugins/callgraph/requests.ml b/src/plugins/callgraph/requests.ml
new file mode 100644
index 0000000000000000000000000000000000000000..cdef7f503d12adab16126d22d094cecf24057518
--- /dev/null
+++ b/src/plugins/callgraph/requests.ml
@@ -0,0 +1,179 @@
+(**************************************************************************)
+(*                                                                        *)
+(*  This file is part of Frama-C.                                         *)
+(*                                                                        *)
+(*  Copyright (C) 2007-2023                                               *)
+(*    CEA (Commissariat à l'énergie atomique et aux énergies              *)
+(*         alternatives)                                                  *)
+(*                                                                        *)
+(*  you can redistribute it and/or modify it under the terms of the GNU   *)
+(*  Lesser General Public License as published by the Free Software       *)
+(*  Foundation, version 2.1.                                              *)
+(*                                                                        *)
+(*  It is distributed in the hope that it will be useful,                 *)
+(*  but WITHOUT ANY WARRANTY; without even the implied warranty of        *)
+(*  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *)
+(*  GNU Lesser General Public License for more details.                   *)
+(*                                                                        *)
+(*  See the GNU Lesser General Public License version 2.1                 *)
+(*  for more details (enclosed in the file licenses/LGPLv2.1).            *)
+(*                                                                        *)
+(**************************************************************************)
+
+open Server
+
+module G = Services.G
+
+(* --- Package declaration --- *)
+
+let package = Package.package ~plugin:"callgraph" ~title:"Callgraph" ()
+
+
+(* --- Helper modules --- *)
+
+module Record () =
+struct
+  module Record = Data.Record
+  type record
+  let record : record Record.signature = Record.signature ()
+  let field name ?(descr = name) =
+    Record.field record ~name ~descr:(Markdown.plain descr)
+  let publish ?descr name =
+    let descr = Option.map Markdown.plain descr in
+    Record.publish record ~package ~name ?descr
+end
+
+module Enum (X: sig type t end) =
+struct
+  module Enum = Data.Enum
+  let dictionary: X.t Enum.dictionary = Enum.dictionary ()
+  let tag name descr = Enum.tag ~name ~descr:(Markdown.plain descr) dictionary
+  let publish lookup name descr =
+    Enum.set_lookup dictionary lookup;
+    Request.dictionary ~package ~name ~descr:(Markdown.plain descr) dictionary
+end
+
+
+(* --- Types --- *)
+
+module Vertex =
+struct
+  include Record ()
+
+  let kf = field "kf" (module Kernel_ast.Function)
+      ~descr: "The function represented by the node"
+  let is_root = field "is_root" Data.jbool
+      ~descr: "whether this node is the root of a service"
+  let root = field "root" (module Kernel_ast.Function)
+      ~descr: "the root of this node's service"
+
+  include (val publish "vertex")
+  type t = Cil_types.kernel_function Service_graph.vertex
+
+  let to_json (v : Cil_types.kernel_function Service_graph.vertex) =
+    default |>
+    set kf v.node |>
+    set is_root v.is_root |>
+    set root v.root.node |>
+    to_json
+
+  let of_json _js = Data.failure "Vertex.of_json not implemented"
+end
+
+module EdgeKind =
+struct
+  include Enum (struct type t = Service_graph.edge end)
+
+  let inter_services =  tag "inter_services" "a call between two services"
+  let inter_functions = tag "inter_functions" "a call inside a service"
+  let both =            tag "both" "both cases above"
+
+  let lookup = function
+    | Service_graph.Inter_services -> inter_services
+    | Inter_functions -> inter_functions
+    | Both -> both
+
+  include (val publish lookup
+              "edgeKind" "Whether the call goes through services or not")
+end
+
+module Edge =
+struct
+  include Record ()
+
+  let src = field "src" (module Kernel_ast.Function)
+  let dst = field "dst" (module Kernel_ast.Function)
+  let kind = field "kind" (module EdgeKind)
+
+  include (val publish "edge")
+  type t = G.E.t
+
+  let to_json (e : t) =
+    default |>
+    set src (G.E.src e).node |>
+    set dst (G.E.dst e).node |>
+    set kind (G.E.label e) |>
+    to_json
+
+  let of_json _js = Data.failure "Edge.of_json not implemented"
+end
+
+module Graph =
+struct
+  include Record ()
+
+  let vertices = field "vertices" (module Data.Jlist (Vertex))
+  let edges = field "edges" (module Data.Jlist (Edge))
+
+  include (val publish "graph" ~descr:"The callgraph of the current project")
+  type t = G.t
+
+  let get_vertices (g : t) =
+    G.fold_vertex (fun v acc -> v :: acc ) g []
+
+  let get_edges (g : t) =
+    G.fold_edges_e (fun v acc -> v :: acc ) g []
+
+  let to_json (g : t) =
+    default |>
+    set vertices (get_vertices g) |>
+    set edges (get_edges g) |>
+    to_json
+
+  let of_json _js = Data.failure "Graph.of_json not implemented"
+end
+
+
+(* --- Requests --- *)
+
+let _signal =
+  States.register_value
+    ~package ~name:"callgraph"
+    ~descr:(Markdown.plain
+              "The current callgraph or an empty graph if it has not been computed yet")
+    ~output:(module Data.Joption (Graph))
+    ~add_hook:Services.add_hook
+    ~get:
+      begin fun () ->
+        if Services.is_computed () then
+          Some (Services.get ())
+        else
+          None
+      end
+    ()
+
+let _signal =
+  States.register_value
+    ~package ~name:"isComputed"
+    ~descr:(Markdown.plain
+              "This boolean is true if the graph has been computed")
+    ~output:(module Data.Jbool)
+    ~add_hook:Services.add_hook
+    ~get:Services.is_computed
+    ()
+
+let () = Request.register ~package
+    ~kind:`EXEC ~name:"compute"
+    ~descr:(Markdown.plain "Compute the callgraph for the current project")
+    ~input:(module Data.Junit) ~output:(module Data.Junit)
+    (fun () -> ignore (Services.get ()))
diff --git a/src/plugins/callgraph/requests.mli b/src/plugins/callgraph/requests.mli
new file mode 100644
index 0000000000000000000000000000000000000000..d68abd32119990046a8bed6a36cfb5e6c3b8c264
--- /dev/null
+++ b/src/plugins/callgraph/requests.mli
@@ -0,0 +1,23 @@
+(**************************************************************************)
+(*                                                                        *)
+(*  This file is part of Frama-C.                                         *)
+(*                                                                        *)
+(*  Copyright (C) 2007-2023                                               *)
+(*    CEA (Commissariat à l'énergie atomique et aux énergies              *)
+(*         alternatives)                                                  *)
+(*                                                                        *)
+(*  you can redistribute it and/or modify it under the terms of the GNU   *)
+(*  Lesser General Public License as published by the Free Software       *)
+(*  Foundation, version 2.1.                                              *)
+(*                                                                        *)
+(*  It is distributed in the hope that it will be useful,                 *)
+(*  but WITHOUT ANY WARRANTY; without even the implied warranty of        *)
+(*  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *)
+(*  GNU Lesser General Public License for more details.                   *)
+(*                                                                        *)
+(*  See the GNU Lesser General Public License version 2.1                 *)
+(*  for more details (enclosed in the file licenses/LGPLv2.1).            *)
+(*                                                                        *)
+(**************************************************************************)
+
+(* This module only register requests for Frama-C Server. Nothing is exported *)
diff --git a/src/plugins/callgraph/services.ml b/src/plugins/callgraph/services.ml
index b2d3f2893fbb6f37313b3472a20777c936386867..1345b6fcdf985cdd78322a2a91fa205a253dc58f 100644
--- a/src/plugins/callgraph/services.ml
+++ b/src/plugins/callgraph/services.ml
@@ -77,9 +77,12 @@ module State =
       let dependencies = [ Cg.self; Kernel.MainFunction.self ]
     end)
 
+module StateHook = Hook.Build (S.Service_graph.Datatype)
+
 (* eta-expansion required to mask optional argument [?project] *)
 let is_computed () = State.is_computed ()
 let self = State.self
+let add_hook = StateHook.extend
 
 let compute () =
   let cg = Cg.get () in
@@ -92,6 +95,7 @@ let compute () =
   in
   let sg = S.compute cg isr_names in
   State.mark_as_computed ();
+  StateHook.apply sg;
   sg
 
 let get () = State.memo compute