Skip to content

Commit bfc6324

Browse files
committed
feat(service-now): added service now block
1 parent 9cf8aae commit bfc6324

File tree

28 files changed

+26707
-4417
lines changed

28 files changed

+26707
-4417
lines changed

apps/docs/content/docs/en/tools/meta.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@
8181
"sentry",
8282
"serper",
8383
"sftp",
84+
"servicenow",
8485
"sharepoint",
8586
"shopify",
8687
"slack",
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
---
2+
title: ServiceNow
3+
description: Create, read, update, and delete ServiceNow records
4+
---
5+
6+
import { BlockInfoCard } from "@/components/ui/block-info-card"
7+
8+
<BlockInfoCard
9+
type="servicenow"
10+
color="#81B5A1"
11+
/>
12+
13+
{/* MANUAL-CONTENT-START:intro */}
14+
[ServiceNow](https://www.servicenow.com/) is a leading cloud-based platform that provides IT service management (ITSM), IT operations management (ITOM), and IT business management (ITBM) solutions. ServiceNow helps organizations automate workflows, manage digital operations, and deliver exceptional employee and customer experiences.
15+
16+
ServiceNow offers a comprehensive platform for managing IT services, incidents, problems, changes, and other business processes. With its flexible table-based architecture, ServiceNow can be customized to manage virtually any type of record or business process across an organization.
17+
18+
Key features of ServiceNow include:
19+
20+
- **IT Service Management**: Comprehensive ITSM capabilities including incident, problem, change, and request management
21+
- **Custom Tables**: Flexible table-based architecture allowing organizations to create custom tables for any business process
22+
- **Workflow Automation**: Powerful workflow engine for automating business processes and approvals
23+
- **REST API**: Robust REST API for programmatic access to ServiceNow data and operations
24+
25+
In Sim, the ServiceNow integration enables your agents to interact directly with ServiceNow records and tables. This allows for powerful automation scenarios such as automated incident creation, ticket updates, user management, and custom table operations. Your agents can create, read, update, and delete records in any ServiceNow table (incidents, tasks, users, custom tables, etc.) programmatically. This integration bridges the gap between your AI workflows and your ServiceNow instance, enabling seamless automation of IT service management tasks and business processes.
26+
{/* MANUAL-CONTENT-END */}
27+
28+
29+
## Usage Instructions
30+
31+
Integrate ServiceNow into the workflow. Can create, read, update, and delete records in any ServiceNow table (incidents, tasks, users, etc.). Supports OAuth 2.0 (recommended) or Basic Auth authentication.
32+
33+
34+
35+
## Tools
36+
37+
### `servicenow_create`
38+
39+
Create a new record in a ServiceNow table
40+
41+
#### Input
42+
43+
| Parameter | Type | Required | Description |
44+
| --------- | ---- | -------- | ----------- |
45+
| `instanceUrl` | string | Yes | ServiceNow instance URL \(e.g., https://instance.service-now.com\) |
46+
| `authMethod` | string | Yes | Authentication method: `oauth` or `basic` |
47+
| `credential` | string | No | ServiceNow OAuth credential ID \(required when authMethod is `oauth`\) |
48+
| `username` | string | No | ServiceNow username \(required when authMethod is `basic`\) |
49+
| `password` | string | No | ServiceNow password \(required when authMethod is `basic`\) |
50+
| `tableName` | string | Yes | Table name \(e.g., incident, task, sys_user\) |
51+
| `fields` | json | Yes | Fields to set on the record \(JSON object\) |
52+
53+
#### Output
54+
55+
| Parameter | Type | Description |
56+
| --------- | ---- | ----------- |
57+
| `record` | json | Created ServiceNow record with sys_id and other fields |
58+
| `metadata` | json | Operation metadata including record count |
59+
60+
### `servicenow_read`
61+
62+
Read records from a ServiceNow table
63+
64+
#### Input
65+
66+
| Parameter | Type | Required | Description |
67+
| --------- | ---- | -------- | ----------- |
68+
| `instanceUrl` | string | Yes | ServiceNow instance URL |
69+
| `username` | string | Yes | ServiceNow username |
70+
| `password` | string | Yes | ServiceNow password |
71+
| `tableName` | string | Yes | Table name \(e.g., incident, task, sys_user\) |
72+
| `sysId` | string | No | Specific record sys_id to retrieve |
73+
| `number` | string | No | Record number \(e.g., INC0010001\) |
74+
| `query` | string | No | Encoded query string \(e.g., "active=true^priority=1"\) |
75+
| `limit` | number | No | Maximum number of records to return |
76+
| `fields` | string | No | Comma-separated list of fields to return |
77+
78+
#### Output
79+
80+
| Parameter | Type | Description |
81+
| --------- | ---- | ----------- |
82+
| `records` | array | Array of ServiceNow records |
83+
| `metadata` | json | Operation metadata including record count |
84+
85+
### `servicenow_update`
86+
87+
Update an existing record in a ServiceNow table
88+
89+
#### Input
90+
91+
| Parameter | Type | Required | Description |
92+
| --------- | ---- | -------- | ----------- |
93+
| `instanceUrl` | string | Yes | ServiceNow instance URL |
94+
| `username` | string | Yes | ServiceNow username |
95+
| `password` | string | Yes | ServiceNow password |
96+
| `tableName` | string | Yes | Table name \(e.g., incident, task, sys_user\) |
97+
| `sysId` | string | Yes | Record sys_id to update |
98+
| `fields` | json | Yes | Fields to update \(JSON object\) |
99+
100+
#### Output
101+
102+
| Parameter | Type | Description |
103+
| --------- | ---- | ----------- |
104+
| `record` | json | Updated ServiceNow record |
105+
| `metadata` | json | Operation metadata including updated fields |
106+
107+
### `servicenow_delete`
108+
109+
Delete a record from a ServiceNow table
110+
111+
#### Input
112+
113+
| Parameter | Type | Required | Description |
114+
| --------- | ---- | -------- | ----------- |
115+
| `instanceUrl` | string | Yes | ServiceNow instance URL |
116+
| `username` | string | Yes | ServiceNow username |
117+
| `password` | string | Yes | ServiceNow password |
118+
| `tableName` | string | Yes | Table name \(e.g., incident, task, sys_user\) |
119+
| `sysId` | string | Yes | Record sys_id to delete |
120+
121+
#### Output
122+
123+
| Parameter | Type | Description |
124+
| --------- | ---- | ----------- |
125+
| `success` | boolean | Whether the deletion was successful |
126+
| `metadata` | json | Operation metadata including deleted sys_id |
127+
128+
129+
130+
## Notes
131+
132+
- Category: `tools`
133+
- Type: `servicenow`
134+
- Authentication: Supports OAuth 2.0 (recommended) via Sim Bot or Basic Auth with username/password
135+
- Table Names: Common tables include `incident`, `task`, `sys_user`, `change_request`, `problem`, etc.
136+
- Query Syntax: Use ServiceNow encoded query syntax (e.g., `active=true^priority=1`) for filtering records
137+
- sys_id: Every ServiceNow record has a unique `sys_id` that is used to identify and reference records
138+

apps/sim/app/api/auth/oauth/utils.ts

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ export async function getOAuthToken(userId: string, providerId: string): Promise
103103
accessToken: account.accessToken,
104104
refreshToken: account.refreshToken,
105105
accessTokenExpiresAt: account.accessTokenExpiresAt,
106+
idToken: account.idToken,
106107
})
107108
.from(account)
108109
.where(and(eq(account.userId, userId), eq(account.providerId, providerId)))
@@ -130,7 +131,14 @@ export async function getOAuthToken(userId: string, providerId: string): Promise
130131

131132
try {
132133
// Use the existing refreshOAuthToken function
133-
const refreshResult = await refreshOAuthToken(providerId, credential.refreshToken!)
134+
// For ServiceNow, pass the instance URL (stored in idToken) for the token endpoint
135+
const instanceUrl =
136+
providerId === 'servicenow' ? (credential.idToken ?? undefined) : undefined
137+
const refreshResult = await refreshOAuthToken(
138+
providerId,
139+
credential.refreshToken!,
140+
instanceUrl
141+
)
134142

135143
if (!refreshResult) {
136144
logger.error(`Failed to refresh token for user ${userId}, provider ${providerId}`, {
@@ -213,9 +221,13 @@ export async function refreshAccessTokenIfNeeded(
213221
if (shouldRefresh) {
214222
logger.info(`[${requestId}] Token expired, attempting to refresh for credential`)
215223
try {
224+
// For ServiceNow, pass the instance URL (stored in idToken) for the token endpoint
225+
const instanceUrl =
226+
credential.providerId === 'servicenow' ? (credential.idToken ?? undefined) : undefined
216227
const refreshedToken = await refreshOAuthToken(
217228
credential.providerId,
218-
credential.refreshToken!
229+
credential.refreshToken!,
230+
instanceUrl
219231
)
220232

221233
if (!refreshedToken) {
@@ -287,7 +299,14 @@ export async function refreshTokenIfNeeded(
287299
}
288300

289301
try {
290-
const refreshResult = await refreshOAuthToken(credential.providerId, credential.refreshToken!)
302+
// For ServiceNow, pass the instance URL (stored in idToken) for the token endpoint
303+
const instanceUrl =
304+
credential.providerId === 'servicenow' ? (credential.idToken ?? undefined) : undefined
305+
const refreshResult = await refreshOAuthToken(
306+
credential.providerId,
307+
credential.refreshToken!,
308+
instanceUrl
309+
)
291310

292311
if (!refreshResult) {
293312
logger.error(`[${requestId}] Failed to refresh token for credential`)
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import { type NextRequest, NextResponse } from 'next/server'
2+
import { getSession } from '@/lib/auth'
3+
import { env } from '@/lib/core/config/env'
4+
import { getBaseUrl } from '@/lib/core/utils/urls'
5+
import { createLogger } from '@/lib/logs/console/logger'
6+
7+
const logger = createLogger('ServiceNowCallback')
8+
9+
export const dynamic = 'force-dynamic'
10+
11+
export async function GET(request: NextRequest) {
12+
const baseUrl = getBaseUrl()
13+
14+
try {
15+
const session = await getSession()
16+
if (!session?.user?.id) {
17+
return NextResponse.redirect(`${baseUrl}/workspace?error=unauthorized`)
18+
}
19+
20+
const { searchParams } = request.nextUrl
21+
const code = searchParams.get('code')
22+
const state = searchParams.get('state')
23+
const error = searchParams.get('error')
24+
const errorDescription = searchParams.get('error_description')
25+
26+
// Handle OAuth errors from ServiceNow
27+
if (error) {
28+
logger.error('ServiceNow OAuth error:', { error, errorDescription })
29+
return NextResponse.redirect(
30+
`${baseUrl}/workspace?error=servicenow_auth_error&message=${encodeURIComponent(errorDescription || error)}`
31+
)
32+
}
33+
34+
const storedState = request.cookies.get('servicenow_oauth_state')?.value
35+
const storedInstanceUrl = request.cookies.get('servicenow_instance_url')?.value
36+
37+
const clientId = env.SERVICENOW_CLIENT_ID
38+
const clientSecret = env.SERVICENOW_CLIENT_SECRET
39+
40+
if (!clientId || !clientSecret) {
41+
logger.error('ServiceNow credentials not configured')
42+
return NextResponse.redirect(`${baseUrl}/workspace?error=servicenow_config_error`)
43+
}
44+
45+
// Validate state parameter
46+
if (!state || state !== storedState) {
47+
logger.error('State mismatch in ServiceNow OAuth callback')
48+
return NextResponse.redirect(`${baseUrl}/workspace?error=servicenow_state_mismatch`)
49+
}
50+
51+
// Validate authorization code
52+
if (!code) {
53+
logger.error('No code received from ServiceNow')
54+
return NextResponse.redirect(`${baseUrl}/workspace?error=servicenow_no_code`)
55+
}
56+
57+
// Validate instance URL
58+
if (!storedInstanceUrl) {
59+
logger.error('No instance URL stored')
60+
return NextResponse.redirect(`${baseUrl}/workspace?error=servicenow_no_instance`)
61+
}
62+
63+
const redirectUri = `${baseUrl}/api/auth/oauth2/callback/servicenow`
64+
65+
// Exchange authorization code for access token
66+
const tokenResponse = await fetch(`${storedInstanceUrl}/oauth_token.do`, {
67+
method: 'POST',
68+
headers: {
69+
'Content-Type': 'application/x-www-form-urlencoded',
70+
},
71+
body: new URLSearchParams({
72+
grant_type: 'authorization_code',
73+
code: code,
74+
redirect_uri: redirectUri,
75+
client_id: clientId,
76+
client_secret: clientSecret,
77+
}).toString(),
78+
})
79+
80+
if (!tokenResponse.ok) {
81+
const errorText = await tokenResponse.text()
82+
logger.error('Failed to exchange code for token:', {
83+
status: tokenResponse.status,
84+
body: errorText,
85+
})
86+
return NextResponse.redirect(`${baseUrl}/workspace?error=servicenow_token_error`)
87+
}
88+
89+
const tokenData = await tokenResponse.json()
90+
const accessToken = tokenData.access_token
91+
const refreshToken = tokenData.refresh_token
92+
const expiresIn = tokenData.expires_in
93+
// ServiceNow always grants 'useraccount' scope but returns empty string
94+
const scope = tokenData.scope || 'useraccount'
95+
96+
logger.info('ServiceNow token exchange successful:', {
97+
hasAccessToken: !!accessToken,
98+
hasRefreshToken: !!refreshToken,
99+
expiresIn,
100+
})
101+
102+
if (!accessToken) {
103+
logger.error('No access token in response')
104+
return NextResponse.redirect(`${baseUrl}/workspace?error=servicenow_no_token`)
105+
}
106+
107+
// Redirect to store endpoint with token data in cookies
108+
const storeUrl = new URL(`${baseUrl}/api/auth/oauth2/servicenow/store`)
109+
110+
const response = NextResponse.redirect(storeUrl)
111+
112+
// Store token data in secure cookies for the store endpoint
113+
response.cookies.set('servicenow_pending_token', accessToken, {
114+
httpOnly: true,
115+
secure: process.env.NODE_ENV === 'production',
116+
sameSite: 'lax',
117+
maxAge: 60, // 1 minute
118+
path: '/',
119+
})
120+
121+
if (refreshToken) {
122+
response.cookies.set('servicenow_pending_refresh_token', refreshToken, {
123+
httpOnly: true,
124+
secure: process.env.NODE_ENV === 'production',
125+
sameSite: 'lax',
126+
maxAge: 60,
127+
path: '/',
128+
})
129+
}
130+
131+
response.cookies.set('servicenow_pending_instance', storedInstanceUrl, {
132+
httpOnly: true,
133+
secure: process.env.NODE_ENV === 'production',
134+
sameSite: 'lax',
135+
maxAge: 60,
136+
path: '/',
137+
})
138+
139+
response.cookies.set('servicenow_pending_scope', scope || '', {
140+
httpOnly: true,
141+
secure: process.env.NODE_ENV === 'production',
142+
sameSite: 'lax',
143+
maxAge: 60,
144+
path: '/',
145+
})
146+
147+
if (expiresIn) {
148+
response.cookies.set('servicenow_pending_expires_in', expiresIn.toString(), {
149+
httpOnly: true,
150+
secure: process.env.NODE_ENV === 'production',
151+
sameSite: 'lax',
152+
maxAge: 60,
153+
path: '/',
154+
})
155+
}
156+
157+
// Clean up OAuth state cookies
158+
response.cookies.delete('servicenow_oauth_state')
159+
response.cookies.delete('servicenow_instance_url')
160+
161+
return response
162+
} catch (error) {
163+
logger.error('Error in ServiceNow OAuth callback:', error)
164+
return NextResponse.redirect(`${baseUrl}/workspace?error=servicenow_callback_error`)
165+
}
166+
}

0 commit comments

Comments
 (0)