Action

Update Call Sheet

Posted by David Degner, Last update about 17 hours ago

UPDATES

about 17 hours ago

updated icon

show all updates...

about 17 hours ago

updated icon

about 17 hours ago

Minor Changes

UpdateCallSheet.scpt

A Drafts AppleScript that intelligently updates existing photography call sheets with new information from selected email messages using Google’s Gemini AI.

https://directory.getdrafts.com/a/2VF
https://github.com/ddegner/EmailsToCallSheet
https://www.daviddegner.com

Overview

This script enhances existing call sheets by analyzing new email communications and merging relevant information while preserving the original structure and formatting. It’s designed for photographers who need to keep their call sheets current as project details evolve through ongoing client correspondence.

How It Works

  1. Reads Current Call Sheet: Extracts the content from the currently open draft in Drafts
  2. Gathers New Emails: Collects selected messages from the macOS Mail app
  3. Intelligent Merging: Uses Gemini AI to identify new information and update only relevant sections
  4. Preserves Structure: Maintains existing markdown formatting, headings, and organization
  5. Updates Draft: Replaces the current draft content using Drafts URL scheme

Key Features

  • Non-Destructive Updates: Only changes fields when new emails provide definitive updates
  • Structure Preservation: Maintains existing markdown formatting and section order
  • Selective Information: Fills in missing fields only when new emails provide clear information
  • Chronological Email Tracking: Appends new emails to the chronological list without duplicating existing entries
  • Intelligent Field Mapping: Recognizes which new information belongs in which call sheet sections

Usage

Step-by-Step Process

  1. Open Existing Call Sheet: Open a call sheet draft in the Drafts app
  2. Select New Emails: Switch to Mail and select the new email messages to incorporate
  3. Run the Action: Execute the UpdateCallSheet action in Drafts
  4. Review Changes: The script will update the current draft with merged information

What Gets Updated

  • New project details mentioned in recent emails
  • Updated dates, times, or locations
  • Additional team members or roles
  • Changed deliverables or specifications
  • Budget updates or clarifications
  • Missing information filled in from new correspondence

What Gets Preserved

  • Existing formatting and markdown structure
  • Previously gathered information unless explicitly contradicted
  • Section headings and organization
  • Original email chronology (new emails appended, not duplicated)

Configuration

property geminiAPIKeyName : "Gemini_API_Key" -- Keychain service name for API key
property geminiModel : "gemini-2.5-pro-preview-03-25" -- Gemini model to use

Technical Implementation

  • Draft UUID Handling: Uses the current draft’s UUID for precise targeting
  • URL Scheme Integration: Leverages Drafts URL scheme for reliable content replacement
  • Safe Text Encoding: Properly encodes content for URL parameters
  • JSON-Based API Calls: Uses Foundation framework for robust API communication
  • Error Recovery: Graceful handling of API failures and malformed responses

Dependencies

  • Google Gemini API Key: Must be stored in macOS Keychain under service name “Gemini_API_Key”
  • Drafts App: Current draft must be open and accessible
  • macOS Mail: New emails must be selected in Mail message viewer
  • Python 3: Required for secure API communication
  • Internet Connection: Needed for Gemini API access

Error Handling

The script includes comprehensive error handling for:
- Missing API Key: Clear message if Keychain access fails
- No Selected Emails: Warning if no messages are selected in Mail
- API Failures: Graceful handling of Gemini API errors
- Draft Access Issues: Error handling for UUID or content access problems
- Network Issues: Timeout and connection error management

Security Features

  • Secure API Key Storage: Uses macOS Keychain for credential management
  • Local Processing: No permanent storage of sensitive email content
  • Controlled Access: Only updates the specific draft being edited
  • Safe URL Encoding: Prevents injection attacks through proper encoding

Best Practices

  • Review Updates: Always review the updated call sheet before finalizing
  • Selective Email Selection: Only select emails with new, relevant information
  • Regular Updates: Use incrementally rather than processing large batches
  • Backup Important Drafts: Consider duplicating critical call sheets before major updates

Troubleshooting

  • No Updates Applied: Verify that selected emails contain new, definitive information
  • Formatting Issues: Check that the original call sheet uses proper markdown structure
  • API Timeouts: Try with fewer emails or check internet connection
  • Permission Errors: Ensure Drafts and Mail have proper automation permissions

This script is part of the EmailsToCallSheet project and works in conjunction with NewCallSheet.scpt for complete call sheet management workflow.

Steps

  • runAppleScript (macOS only)

    use framework "Foundation"
    use scripting additions
    
    -- ==========================================
    -- Drafts-only AppleScript Action (macOS)
    -- Amend current call sheet from selected Mail messages via Gemini
    -- Simple version: URL-scheme writeback only (no object refs)
    -- ==========================================
    
    -- === USER SETTINGS ===
    property geminiAPIKeyName : "Gemini_API_Key" -- Keychain service name
    property geminiModel : "gemini-3-flash-preview" -- Gemini model id
    
    on execute(d)
    	try
    		-- Current draft content + uuid from Drafts-supplied record
    		set callsheetText to ""
    		try
    			set callsheetText to (content of d)
    		on error
    			set callsheetText to ""
    		end try
    		set theUUID to my getUUIDFromRecord(d)
    		if theUUID is "" then error "Could not read the current draft UUID."
    
    		-- Gather selected Mail messages as plain text
    		set mailText to my getSelectedMailThreadText()
    		if mailText is "" then error "No messages are selected in Mail."
    
    		-- Build prompt merging existing call sheet and new emails
    		set promptText to my buildPrompt(callsheetText, mailText)
    
    		-- Call Gemini for updated call sheet markdown
    		set updatedText to my callGemini(promptText)
    		if updatedText is "" then error "Gemini returned empty text."
    
    		-- Write back to THIS draft using Drafts URL scheme only
    		set encodedText to my encodeURIComponent(updatedText)
    		set L to (length of callsheetText)
    		if L < 0 then set L to 0
    		set u to "drafts://x-callback-url/replaceRange?uuid=" & theUUID & "&text=" & encodedText & "&start=0&length=" & (L as text)
    		open location u
    
    		return ""
    	on error errMsg number errNum
    		display dialog ("An error occurred: " & errMsg & " (" & errNum & ")") buttons {"OK"} default button 1 with icon caution
    		return ""
    	end try
    end execute
    
    
    on getSelectedMailThreadText()
    	-- Returns concatenated plain text for selected messages in Mail.
    	set msgList to {}
    	tell application "Mail"
    		try
    			set msgList to selected messages of message viewer 1
    		on error
    			set msgList to {}
    		end try
    		if msgList is {} then
    			try
    				set msgList to selection
    			on error
    				set msgList to {}
    			end try
    		end if
    		if msgList is {} then return ""
    		set collected to {}
    		repeat with msg in msgList
    			set fromLine to "From: " & (sender of msg as text)
    			set dateLine to "Date: " & ((date sent of msg) as text)
    			set subjLine to "Subject: " & (subject of msg as text)
    			set bodyText to (content of msg as text)
    			set entry to fromLine & return & dateLine & return & subjLine & return & return & bodyText
    			set end of collected to entry
    		end repeat
    	end tell
    	set oldTID to AppleScript's text item delimiters
    	set AppleScript's text item delimiters to (return & return & "----- EMAIL BREAK -----" & return & return)
    	set joined to collected as text
    	set AppleScript's text item delimiters to oldTID
    	return joined
    end getSelectedMailThreadText
    
    
    on buildPrompt(existingCallsheet, newEmails)
    	set intro to "You are a meticulous production coordinator. Update the EXISTING CALL SHEET with any new information found in the NEW EMAIL THREAD. Keep the existing structure and formatting. Only change fields when the new emails provide definitive updates; otherwise leave them as-is. If a field is missing and the emails provide new information, fill it in."
    	set rules to "Update the section titled 'Chronological Email List' by appending only entries for emails NOT already present. Do not duplicate existing items. Preserve and replicate current markdown headings, formatting and spacing and order emails oldest→newest. Output ONLY the updated call sheet and emails; no extra commentary."
    	set s to intro & return & return & rules & return & return & "===== EXISTING CALL SHEET =====" & return & existingCallsheet & return & "===== END EXISTING CALL SHEET =====" & return & return & "===== NEW EMAIL THREAD =====" & return & newEmails & return & "===== END NEW EMAIL THREAD ====="
    	return s
    end buildPrompt
    
    
    on callGemini(promptText)
    	set apiKey to my readKeychain(geminiAPIKeyName)
    	if apiKey is "" then error "Gemini API key not found in Keychain (service: " & geminiAPIKeyName & ")."
    
    	-- Build request JSON with Cocoa (safe escaping)
    	set dict to current application's NSMutableDictionary's dictionary()
    	set contentsArr to current application's NSMutableArray's array()
    
    	set partsArr to current application's NSMutableArray's array()
    	set partDict to current application's NSMutableDictionary's dictionary()
    	partDict's setObject:promptText forKey:"text"
    	partsArr's addObject:partDict
    
    	set contentDict to current application's NSMutableDictionary's dictionary()
    	contentDict's setObject:"user" forKey:"role"
    	contentDict's setObject:partsArr forKey:"parts"
    	contentsArr's addObject:contentDict
    
    	dict's setObject:contentsArr forKey:"contents"
    
    	set genCfg to current application's NSMutableDictionary's dictionary()
    	genCfg's setObject:(current application's NSNumber's numberWithDouble:0.2) forKey:"temperature"
    	genCfg's setObject:(current application's NSNumber's numberWithInteger:16384) forKey:"maxOutputTokens"
    	dict's setObject:genCfg forKey:"generationConfig"
    
    	set jsonData to current application's NSJSONSerialization's dataWithJSONObject:dict options:0 |error|:(missing value)
    	set jsonString to (current application's NSString's alloc()'s initWithData:jsonData encoding:(current application's NSUTF8StringEncoding)) as text
    
    	-- Header-based API key; avoid 'url' var name
    	set endpointStr to "https://generativelanguage.googleapis.com/v1beta/models/" & geminiModel & ":generateContent"
    	set curlCmd to "/usr/bin/curl -sS -X POST -H 'Content-Type: application/json' -H " & quoted form of ("x-goog-api-key: " & apiKey) & " --data " & quoted form of jsonString & " " & quoted form of endpointStr
    	set respText to do shell script curlCmd
    
    	-- Parse response JSON and extract concatenated text parts
    	set respNSString to current application's NSString's stringWithString:respText
    	set respData to respNSString's dataUsingEncoding:(current application's NSUTF8StringEncoding)
    	set respObj to current application's NSJSONSerialization's JSONObjectWithData:respData options:0 |error|:(missing value)
    
    	set candidates to respObj's objectForKey:"candidates"
    	if (candidates = missing value) or ((candidates's |count|()) = 0) then error "Gemini returned no candidates."
    	set firstCand to candidates's objectAtIndex:0
    	set contentDict2 to firstCand's objectForKey:"content"
    	set partsArray2 to contentDict2's objectForKey:"parts"
    	if (partsArray2's |count|()) = 0 then error "Gemini returned no text parts."
    	set outText to ""
    	repeat with i from 0 to ((partsArray2's |count|()) - 1)
    		set p to (partsArray2's objectAtIndex:i)
    		set t to p's objectForKey:"text"
    		if t is not missing value then set outText to outText & (t as text)
    	end repeat
    	return outText as text
    end callGemini
    
    
    on readKeychain(serviceName)
    	set cmd to "security find-generic-password -s " & quoted form of serviceName & " -w"
    	try
    		set k to do shell script cmd
    		return k
    	on error
    		return ""
    	end try
    end readKeychain
    
    
    on encodeURIComponent(t)
    	set ns to current application's NSString's stringWithString:t
    	set allowed to current application's NSCharacterSet's URLQueryAllowedCharacterSet()
    	set m to allowed's mutableCopy()
    	m's removeCharactersInString:"&=?+" -- conservative for query values
    	set enc to ns's stringByAddingPercentEncodingWithAllowedCharacters:m
    	return enc as text
    end encodeURIComponent
    
    
    on getUUIDFromRecord(r)
    	try
    		return |uuid| of r
    	on error
    		try
    			return uuid of r
    		on error
    			return ""
    		end try
    	end try
    end getUUIDFromRecord
    
  • configure

    draftList
    noChange
    actionList
    noChange
    actionBar
    noChange
    tagEntry
    noChange
    loadActionGroup
    loadActionBarGroup
    loadWorkspace
    Call Sheets
    linksEnabled
    noChange
    pinningEnabled
    noChange

Options

  • After Success Default
    Notification Info
    Log Level Info
Items available in the Drafts Directory are uploaded by community members. Use appropriate caution reviewing downloaded items before use.