Skip to content

Commit 6827be7

Browse files
authored
feat(google_docs): opt-in Markdown formatting for create operation (#4656)
* feat(google_docs): opt-in Markdown formatting for create operation * fix(google_docs): harden multipart boundary handoff and align postProcess guard
1 parent a0d9e4d commit 6827be7

4 files changed

Lines changed: 91 additions & 12 deletions

File tree

apps/sim/blocks/blocks/google_docs.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,15 @@ Return ONLY the document content - no explanations, no extra text.`,
155155
placeholder: 'Describe the document content you want to create...',
156156
},
157157
},
158+
// Markdown formatting toggle for create operation
159+
{
160+
id: 'markdown',
161+
title: 'Interpret content as Markdown',
162+
type: 'switch',
163+
condition: { field: 'operation', value: 'create' },
164+
description:
165+
'Convert headings, bold/italic, lists, tables, links, code, and blockquotes into formatted Google Docs content. When off, content is inserted as plain text.',
166+
},
158167
],
159168
tools: {
160169
access: ['google_docs_read', 'google_docs_write', 'google_docs_create'],
@@ -193,6 +202,10 @@ Return ONLY the document content - no explanations, no extra text.`,
193202
title: { type: 'string', description: 'Document title' },
194203
folderId: { type: 'string', description: 'Parent folder identifier (canonical param)' },
195204
content: { type: 'string', description: 'Document content' },
205+
markdown: {
206+
type: 'boolean',
207+
description: 'Interpret content as Markdown when creating a document',
208+
},
196209
},
197210
outputs: {
198211
content: { type: 'string', description: 'Document content' },

apps/sim/tools/google_docs/create.ts

Lines changed: 75 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,37 @@
11
import { createLogger } from '@sim/logger'
2+
import { generateShortId } from '@sim/utils/id'
23
import type { GoogleDocsCreateResponse, GoogleDocsToolParams } from '@/tools/google_docs/types'
34
import type { ToolConfig } from '@/tools/types'
45

56
const logger = createLogger('GoogleDocsCreateTool')
67

8+
const DOC_MIME_TYPE = 'application/vnd.google-apps.document'
9+
10+
/**
11+
* Build a multipart/related body for Drive's files.create upload endpoint.
12+
* Used when converting Markdown to a Google Doc in a single round-trip.
13+
* See: https://developers.google.com/workspace/drive/api/guides/manage-uploads
14+
*/
15+
function buildMarkdownMultipartBody(
16+
metadata: Record<string, unknown>,
17+
markdownContent: string,
18+
boundary: string
19+
): string {
20+
return (
21+
`--${boundary}\r\n` +
22+
`Content-Type: application/json; charset=UTF-8\r\n\r\n` +
23+
`${JSON.stringify(metadata)}\r\n` +
24+
`--${boundary}\r\n` +
25+
`Content-Type: text/markdown\r\n\r\n` +
26+
`${markdownContent}\r\n` +
27+
`--${boundary}--`
28+
)
29+
}
30+
31+
function shouldUseMarkdownUpload(params: GoogleDocsToolParams): boolean {
32+
return Boolean(params.markdown && params.content)
33+
}
34+
735
export const createTool: ToolConfig<GoogleDocsToolParams, GoogleDocsCreateResponse> = {
836
id: 'google_docs_create',
937
name: 'Create Google Docs Document',
@@ -46,19 +74,37 @@ export const createTool: ToolConfig<GoogleDocsToolParams, GoogleDocsCreateRespon
4674
visibility: 'hidden',
4775
description: 'The ID of the folder to create the document in (internal use)',
4876
},
77+
markdown: {
78+
type: 'boolean',
79+
required: false,
80+
visibility: 'user-or-llm',
81+
description:
82+
'When true, content is interpreted as Markdown and converted to formatted Google Docs content (headings, bold/italic, lists, tables, links, code blocks, blockquotes). Default: false (content inserted as plain text).',
83+
},
4984
},
5085

5186
request: {
52-
url: () => {
53-
return 'https://www.googleapis.com/drive/v3/files?supportsAllDrives=true'
87+
url: (params) => {
88+
return shouldUseMarkdownUpload(params)
89+
? 'https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart&supportsAllDrives=true'
90+
: 'https://www.googleapis.com/drive/v3/files?supportsAllDrives=true'
5491
},
5592
method: 'POST',
5693
headers: (params) => {
57-
// Validate access token
5894
if (!params.accessToken) {
5995
throw new Error('Access token is required')
6096
}
6197

98+
if (shouldUseMarkdownUpload(params)) {
99+
const boundary = `sim_gdocs_md_${generateShortId(24)}`
100+
// Stash on params so body() uses the matching boundary string
101+
;(params as GoogleDocsToolParams & { _boundary?: string })._boundary = boundary
102+
return {
103+
Authorization: `Bearer ${params.accessToken}`,
104+
'Content-Type': `multipart/related; boundary=${boundary}`,
105+
}
106+
}
107+
62108
return {
63109
Authorization: `Bearer ${params.accessToken}`,
64110
'Content-Type': 'application/json',
@@ -69,18 +115,30 @@ export const createTool: ToolConfig<GoogleDocsToolParams, GoogleDocsCreateRespon
69115
throw new Error('Title is required')
70116
}
71117

72-
const requestBody: any = {
118+
const folderId = params.folderSelector || params.folderId
119+
const metadata: Record<string, unknown> = {
73120
name: params.title,
74-
mimeType: 'application/vnd.google-apps.document',
121+
mimeType: DOC_MIME_TYPE,
75122
}
76-
77-
// Add parent folder if specified (prefer folderSelector over folderId)
78-
const folderId = params.folderSelector || params.folderId
79123
if (folderId) {
80-
requestBody.parents = [folderId]
124+
metadata.parents = [folderId]
125+
}
126+
127+
if (shouldUseMarkdownUpload(params)) {
128+
const boundary = (params as GoogleDocsToolParams & { _boundary?: string })._boundary
129+
if (!boundary) {
130+
// headers() runs before body() in formatRequestParams and stashes the boundary
131+
// on the same params reference. Missing _boundary means that contract was broken,
132+
// which would silently produce a Content-Type / body boundary mismatch (HTTP 400).
133+
// Throw loudly instead of fabricating a mismatched boundary.
134+
throw new Error(
135+
'Multipart boundary missing on params — headers() must run before body() for markdown upload'
136+
)
137+
}
138+
return buildMarkdownMultipartBody(metadata, params.content ?? '', boundary)
81139
}
82140

83-
return requestBody
141+
return metadata
84142
},
85143
},
86144

@@ -91,6 +149,12 @@ export const createTool: ToolConfig<GoogleDocsToolParams, GoogleDocsCreateRespon
91149

92150
const documentId = result.output.metadata.documentId
93151

152+
// When the markdown upload path ran, content was already inserted via Drive's
153+
// text/markdown import conversion during files.create — no follow-up write needed.
154+
if (shouldUseMarkdownUpload(params)) {
155+
return result
156+
}
157+
94158
if (params.content && documentId) {
95159
try {
96160
const writeParams = {
@@ -128,7 +192,7 @@ export const createTool: ToolConfig<GoogleDocsToolParams, GoogleDocsCreateRespon
128192
const metadata = {
129193
documentId,
130194
title: title || 'Untitled Document',
131-
mimeType: 'application/vnd.google-apps.document',
195+
mimeType: DOC_MIME_TYPE,
132196
url: `https://docs.google.com/document/d/${documentId}/edit`,
133197
}
134198

apps/sim/tools/google_docs/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export interface GoogleDocsToolParams {
3737
content?: string
3838
folderId?: string
3939
folderSelector?: string
40+
markdown?: boolean
4041
}
4142

4243
export type GoogleDocsResponse =

apps/sim/tools/google_docs/write.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import type { ToolConfig } from '@/tools/types'
44
export const writeTool: ToolConfig<GoogleDocsToolParams, GoogleDocsWriteResponse> = {
55
id: 'google_docs_write',
66
name: 'Write to Google Docs Document',
7-
description: 'Write or update content in a Google Docs document',
7+
description:
8+
'Append content to a Google Docs document. Content is inserted literally; Markdown is not interpreted. For formatted output from Markdown, use the Create operation with the markdown toggle enabled.',
89
version: '1.0',
910
oauth: {
1011
required: true,

0 commit comments

Comments
 (0)