diff --git a/docs/user/assistant.md b/docs/user/assistant.md
index 2e87c067a6..7fd20dcc07 100644
--- a/docs/user/assistant.md
+++ b/docs/user/assistant.md
@@ -49,3 +49,10 @@ to generate code directly within the editor.
 This is useful when you want to quickly add code to an
 existing function node without having to generate a full function node from scratch.
 
+### In-line JSON Generation
+
+The FlowFuse Assistant plugin also adds a new code lens to the JSON editor that allows you
+to generate JSON directly within the monaco editor.
+
+This is useful when you want to quickly generate JSON in a template node, change node, inject node or
+any node that the TypedInput offers the JSON editor.
diff --git a/forge/lib/permissions.js b/forge/lib/permissions.js
index a431db417b..f49d9a6251 100644
--- a/forge/lib/permissions.js
+++ b/forge/lib/permissions.js
@@ -112,7 +112,7 @@ const Permissions = {
     'platform:audit-log': { description: 'View platform audit log', role: Roles.Admin },
 
     // assistant
-    'assistant:function': { description: 'Access the assistant function endpoint', role: Roles.Member }
+    'assistant:method': { description: 'Access the assistant method endpoint', role: Roles.Member }
 }
 
 module.exports = {
diff --git a/forge/routes/api/assistant.js b/forge/routes/api/assistant.js
index 04200ad02a..658159cac0 100644
--- a/forge/routes/api/assistant.js
+++ b/forge/routes/api/assistant.js
@@ -12,13 +12,13 @@ module.exports = async function (app) {
     app.addHook('preHandler', app.verifySession)
 
     /**
-     * Endpoint for assistant functions
+     * Endpoint for assistant methods
      * For now, this is simply a relay to an external assistant service
      * In the future, we may decide to bring that service inside the core or
      * use an alternative means of accessing it.
     */
-    app.post('/function', {
-        preHandler: app.needsPermission('assistant:function'),
+    app.post('/:method', {
+        preHandler: app.needsPermission('assistant:method'),
         schema: {
             hide: true, // dont show in swagger
             body: {
@@ -45,8 +45,10 @@ module.exports = async function (app) {
         }
     },
     async (request, reply) => {
-        // const method = request.params.method // FUTURE: allow for different methods
-        const method = 'function' // for now, only function node/code generation is supported
+        const method = request.params.method // the method to call at the assistant service
+        if (/^[a-z0-9_-]+$/.test(method) === false) {
+            return reply.code(400).send({ code: 'invalid_method', error: 'Invalid method name' })
+        }
 
         const serviceUrl = app.config.assistant?.service?.url
         const serviceToken = app.config.assistant?.service?.token
diff --git a/forge/routes/auth/permissions.js b/forge/routes/auth/permissions.js
index 7a463d47ab..7abf8e1b58 100644
--- a/forge/routes/auth/permissions.js
+++ b/forge/routes/auth/permissions.js
@@ -10,7 +10,7 @@ const IMPLICIT_TOKEN_SCOPES = {
         'team:projects:list', // permit a device being edited via a tunnel in developer mode to list projects
         'library:entry:create', // permit a device being edited via a tunnel in developer mode to create library entries
         'library:entry:list', // permit a device being edited via a tunnel in developer mode to list library entries
-        'assistant:function' // permit calls to the assistant endpoint for function node/code creation
+        'assistant:method' // permit calls to the assistant endpoint for method node/code/json/etc creation
     ],
     project: [
         'user:read',
@@ -19,7 +19,7 @@ const IMPLICIT_TOKEN_SCOPES = {
         'team:projects:list',
         'library:entry:create',
         'library:entry:list',
-        'assistant:function'
+        'assistant:method'
     ]
 }
 
diff --git a/test/unit/forge/routes/api/assistant_spec.js b/test/unit/forge/routes/api/assistant_spec.js
index 3e715fb06b..8afd479020 100644
--- a/test/unit/forge/routes/api/assistant_spec.js
+++ b/test/unit/forge/routes/api/assistant_spec.js
@@ -92,73 +92,112 @@ describe('Assistant API', async function () {
             response.statusCode.should.equal(501)
         })
     })
-    describe('function service', async function () {
-        it('anonymous cannot access /function', async function () {
-            const response = await app.inject({
-                method: 'GET',
-                url: '/api/v1/assistant/function'
-            })
-            response.statusCode.should.equal(401)
-        })
-        it('random token cannot access /function', async function () {
-            const response = await app.inject({
-                method: 'GET',
-                url: '/api/v1/assistant/function',
-                headers: { authorization: 'Bearer blah-blah' }
-            })
-            response.statusCode.should.equal(401)
-        })
-        it('device token can access /function', async function () {
-            // const device = await createDevice({ name: 'Ad1', type: 'Ad1_type', team: TestObjects.ATeam.hashid, as: TestObjects.tokens.alice })
-            const deviceCreateResponse = await app.inject({
-                method: 'POST',
-                url: '/api/v1/devices',
-                body: {
-                    name: 'Ad1',
-                    type: 'Ad1_type',
-                    team: TestObjects.ATeam.hashid
-                },
-                cookies: { sid: TestObjects.tokens.alice }
-            })
-            const device = deviceCreateResponse.json()
-            sinon.stub(axios, 'post').resolves({ data: { status: 'ok' } })
+
+    describe('method constraints', async function () {
+        it('should return 400 if method contains capital letters', async function () {
             const response = await app.inject({
                 method: 'POST',
-                url: '/api/v1/assistant/function',
-                headers: { authorization: 'Bearer ' + device.credentials.token },
+                url: '/api/v1/assistant/InvalidMethod',
+                headers: { authorization: 'Bearer ' + TestObjects.tokens.instance },
                 payload: { prompt: 'multiply by 5', transactionId: '1234' }
             })
-            axios.post.calledOnce.should.be.true()
-            response.statusCode.should.equal(200)
+            response.statusCode.should.equal(400)
         })
-        it('instance token can access /function', async function () {
-            sinon.stub(axios, 'post').resolves({ data: { status: 'ok' } })
+        it('should return 400 if method contains invalid characters', async function () {
             const response = await app.inject({
                 method: 'POST',
-                url: '/api/v1/assistant/function',
+                url: '/api/v1/assistant/inv@lid',
                 headers: { authorization: 'Bearer ' + TestObjects.tokens.instance },
                 payload: { prompt: 'multiply by 5', transactionId: '1234' }
             })
-            axios.post.calledOnce.should.be.true()
-            response.statusCode.should.equal(200)
+            response.statusCode.should.equal(400)
         })
-        it('fails when prompt is not supplied', async function () {
+        it('should return 400 if method contains escaped characters', async function () {
             const response = await app.inject({
                 method: 'POST',
-                url: '/api/v1/assistant/function',
+                url: '/api/v1/assistant/method%2Fwith%2Fslashes',
                 headers: { authorization: 'Bearer ' + TestObjects.tokens.instance },
-                payload: { transactionId: '1234' }
+                payload: { prompt: 'multiply by 5', transactionId: '1234' }
             })
             response.statusCode.should.equal(400)
         })
-        it('fails when transactionId is not supplied', async function () {
-            const response = await app.inject({
-                method: 'POST',
-                url: '/api/v1/assistant/function',
-                headers: { authorization: 'Bearer ' + TestObjects.tokens.instance },
-                payload: { prompt: 'multiply by 5' }
+    })
+
+    describe('service tests', async function () {
+        function serviceTests (serviceName) {
+            it('anonymous cannot access', async function () {
+                const response = await app.inject({
+                    method: 'GET',
+                    url: `/api/v1/assistant/${serviceName}`
+                })
+                response.statusCode.should.equal(401)
             })
-            response.statusCode.should.equal(400)
+            it('random token cannot access', async function () {
+                const response = await app.inject({
+                    method: 'GET',
+                    url: `/api/v1/assistant/${serviceName}`,
+                    headers: { authorization: 'Bearer blah-blah' }
+                })
+                response.statusCode.should.equal(401)
+            })
+            it('device token can access', async function () {
+                // const device = await createDevice({ name: 'Ad1', type: 'Ad1_type', team: TestObjects.ATeam.hashid, as: TestObjects.tokens.alice })
+                const deviceCreateResponse = await app.inject({
+                    method: 'POST',
+                    url: '/api/v1/devices',
+                    body: {
+                        name: 'Ad1',
+                        type: 'Ad1_type',
+                        team: TestObjects.ATeam.hashid
+                    },
+                    cookies: { sid: TestObjects.tokens.alice }
+                })
+                const device = deviceCreateResponse.json()
+                sinon.stub(axios, 'post').resolves({ data: { status: 'ok' } })
+                const response = await app.inject({
+                    method: 'POST',
+                    url: `/api/v1/assistant/${serviceName}`,
+                    headers: { authorization: 'Bearer ' + device.credentials.token },
+                    payload: { prompt: 'multiply by 5', transactionId: '1234' }
+                })
+                axios.post.calledOnce.should.be.true()
+                response.statusCode.should.equal(200)
+            })
+            it('instance token can access', async function () {
+                sinon.stub(axios, 'post').resolves({ data: { status: 'ok' } })
+                const response = await app.inject({
+                    method: 'POST',
+                    url: `/api/v1/assistant/${serviceName}`,
+                    headers: { authorization: 'Bearer ' + TestObjects.tokens.instance },
+                    payload: { prompt: 'multiply by 5', transactionId: '1234' }
+                })
+                axios.post.calledOnce.should.be.true()
+                response.statusCode.should.equal(200)
+            })
+            it('fails when prompt is not supplied', async function () {
+                const response = await app.inject({
+                    method: 'POST',
+                    url: `/api/v1/assistant/${serviceName}`,
+                    headers: { authorization: 'Bearer ' + TestObjects.tokens.instance },
+                    payload: { transactionId: '1234' }
+                })
+                response.statusCode.should.equal(400)
+            })
+            it('fails when transactionId is not supplied', async function () {
+                const response = await app.inject({
+                    method: 'POST',
+                    url: `/api/v1/assistant/${serviceName}`,
+                    headers: { authorization: 'Bearer ' + TestObjects.tokens.instance },
+                    payload: { prompt: 'multiply by 5' }
+                })
+                response.statusCode.should.equal(400)
+            })
+        }
+        describe('function service', async function () {
+            serviceTests('function')
+        })
+        describe('json service', async function () {
+            serviceTests('json')
         })
     })
 })