Skip to content

Update Contact Tool

The update_contact tool lets the plan executor update contact profile fields during execution. Unlike send_email and send_sms, it executes immediately without approval — the executor writes field changes directly to the database.

The tool is only available when the Update Contact action is assigned to the plan template. Without it, the executor cannot see or call the tool.

-- Seeded action record
INSERT INTO "action" (id, name, required_tools)
VALUES ('a0000000-0000-0000-0000-000000000004', 'Update Contact', '["update_contact"]');

When you add this action to a template, its prompt_fragment is injected into the executor’s system prompt, instructing the LLM to use the tool directly whenever it learns new information about a contact.

The tool accepts any combination of these optional fields. Omitted fields remain unchanged.

FieldTypeValidation
contact_idString (UUID)Required. Must belong to the plan.
first_nameString
last_nameString
companyString
job_titleString
timezoneStringIANA timezone (e.g. America/New_York). Validated against chrono_tz.
preferred_languageString
genderStringmale, female, non-binary
pipeline_stageStringlead, prospect, qualified, customer, churned
date_of_birthStringISO 8601 date (e.g. 1990-05-15). Validated against NaiveDate.
sourceString
LLM calls update_contact({ contact_id, company: "Acme Inc", pipeline_stage: "qualified" })
→ Parse UUID
→ Guard: contact must be in this plan's contact list
→ Build ContactFieldUpdates with only provided fields
→ Validate timezone (chrono_tz) and date_of_birth (NaiveDate)
→ Write to database (selective UPDATE, only non-None fields)
→ Log UpdateContact entry with field→value changes
→ Return: "Contact updated: company → Acme Inc, pipeline stage → qualified"

The tool only updates fields you provide — it never clears existing values.

The update logic lives in update_contact_fields_service.rs:

pub struct ContactFieldUpdates {
pub first_name: Option<String>,
pub last_name: Option<String>,
pub company: Option<String>,
pub job_title: Option<String>,
pub timezone: Option<String>,
pub preferred_language: Option<String>,
pub gender: Option<String>,
pub pipeline_stage: Option<String>,
pub date_of_birth: Option<String>,
pub source: Option<String>,
pub external_id: Option<String>,
}
pub async fn update_contact_fields(
contact_id: Uuid,
organization_id: Uuid,
fields: ContactFieldUpdates,
) -> Result<Vec<String>, AppError>

Returns a Vec<String> of human-readable change descriptions (e.g. "company → Acme Inc").

Updates are logged in the plan timeline with structured input/output:

pub struct UpdateContactInput {
pub contact_id: Uuid,
pub fields: HashMap<String, String>,
}
pub struct UpdateContactOutput {
pub changes: Vec<String>,
}

The entry uses ToolCallRole::PostAction — it attaches to the previous primary node (like send_email) in the graph view rather than appearing as a standalone node.

Update contact entries appear in both the timeline and graph views:

  • Icon: UserPen (lucide)
  • Color: Emerald — same as write_interaction_note and update_system_note
  • Label: “Update Contact”
  • Detail: Lists each field change as "field name → new value"
ErrorResponse
Invalid UUID"Invalid contact_id UUID: {error}"
Contact not in plan"Contact not associated with this plan"
Invalid timezoneAppError::InvalidInput — bad IANA timezone string
Invalid dateAppError::InvalidInput — bad ISO 8601 date string
FilePurpose
migration/src/m20260324_120001_seed_update_contact_action.rsSeeds the action record
src/mods/contact/services/update_contact_fields_service.rsField update logic + validation
src/mods/plan/types/plan_tool_type.rsPlanTool::UpdateContact enum variant
src/mods/plan/tools/build_tools.rsTool schema + execution handler
src/mods/plan/types/plan_log_entry_type.rsLog entry types + PostAction role