Use this file to discover all available pages before exploring further.
Actions transform your Guru from a static knowledge base into a dynamic AI agent that can execute code, call APIs, and fetch real-time data. When users ask questions, your Guru automatically triggers relevant actions to provide live, accurate responses.
Pain: Billing questions are high-volume. Agents waste time hopping between Stripe, ERP, CRM, and email threads just to answer “where is my refund?”User asks:“Where is my refund for order 12345?” or “Can you resend the invoice for December?”Integrations: Stripe, Billing/ERP, CRMResult: Refund state + ETA + invoice link + next step (if failed)Impact: Reduces billing tickets 40%+, eliminates “let me check and get back to you”Trigger: “When the user asks about a refund status, invoice, or payment”Parameters:
Name
Type
What to Extract
Required
order_id
String
Order or invoice number
Yes
customer_email
String
Customer’s email address
No
Secrets:STRIPE_API_KEY
import osimport requestsorder_id = os.environ.get('order_id')api_key = os.environ.get('STRIPE_API_KEY')# Search for refunds related to this orderresponse = requests.get( "https://api.stripe.com/v1/refunds", auth=(api_key, ''), params={"limit": 10})refunds = response.json().get('data', [])found = Nonefor r in refunds: if order_id in str(r.get('metadata', {})) or order_id in r.get('id', ''): found = r breakif found: status = found['status'] amount = found['amount'] / 100 currency = found['currency'].upper() print(f"💰 Refund Status: {status.upper()}") print(f"💵 Amount: {amount} {currency}") if status == 'succeeded': print(f"✓ Refund completed - funds returned to original payment method") print(f"⏱️ May take 5-10 business days to appear on statement") elif status == 'pending': print(f"⏳ Refund is processing - typically completes within 5-10 business days") else: print(f"⚠️ Refund status: {status} - may require manual review")else: print(f"No refund found for order {order_id}") print(f"💡 Next step: Check if refund was requested or process new refund")
Double Charge Investigation
Pain: “I was charged twice!” claims escalate fast. Agents need exact transaction mapping to prevent unnecessary refunds.User asks:“I see two charges of $49.99. What happened?”Integrations: Payment provider, Subscription ledgerResult: Charge IDs, whether one is auth/hold, whether it was reversed, recommended responseImpact: Prevents unnecessary refunds, reduces escalations, improves customer trustTrigger: “When the user reports duplicate charges, double billing, or being charged twice”Parameters:
Name
Type
What to Extract
Required
customer_email
String
Customer’s email address
Yes
amount
Number
The charge amount in question
No
Secrets:STRIPE_API_KEY
import osimport requestsfrom datetime import datetime, timedeltaemail = os.environ.get('customer_email')amount = os.environ.get('amount')api_key = os.environ.get('STRIPE_API_KEY')# Get recent charges for this customerresponse = requests.get( "https://api.stripe.com/v1/charges", auth=(api_key, ''), params={"limit": 20})charges = response.json().get('data', [])recent = [c for c in charges if email in str(c.get('billing_details', {}).get('email', ''))]if len(recent) >= 2: print(f"📋 Found {len(recent)} recent charges for {email}:\n") for c in recent[:5]: amt = c['amount'] / 100 status = c['status'] captured = "Captured" if c['captured'] else "Auth Hold (not captured)" refunded = " [REFUNDED]" if c['refunded'] else "" date = datetime.fromtimestamp(c['created']).strftime('%Y-%m-%d %H:%M') print(f" • ${amt} - {status} - {captured}{refunded}") print(f" ID: {c['id'][:20]}... | Date: {date}\n") # Check for auth holds auth_holds = [c for c in recent if not c['captured']] if auth_holds: print(f"💡 {len(auth_holds)} charge(s) are authorization holds that will auto-release in 7 days") refunded = [c for c in recent if c['refunded']] if refunded: print(f"✓ {len(refunded)} charge(s) already refunded")else: print(f"Only {len(recent)} charge(s) found for {email} - no duplicate detected")
Outage & Incident Status
Pain: During outages, agents guess and customers get conflicting answers. Tickets pile up with the same question.User asks:“Your app is down!” or “502 error - is this known?”Integrations: Statuspage, Datadog/New Relic, Incident channelResult: Current incident status, impacted regions/features, workaround, ETA, subscribe linkImpact: Deflects 80%+ of duplicate incident tickets, standardizes incident communicationTrigger: “When the user reports an error, outage, downtime, or asks if there’s a known issue”Parameters:
Name
Type
What to Extract
Required
error_code
String
Error code or message (e.g., 502, timeout)
No
feature
String
Feature or service affected
No
Secrets:STATUSPAGE_API_KEY, STATUSPAGE_PAGE_ID
import osimport requestsapi_key = os.environ.get('STATUSPAGE_API_KEY')page_id = os.environ.get('STATUSPAGE_PAGE_ID')error_code = os.environ.get('error_code', '')feature = os.environ.get('feature', '')# Get current incidentsresponse = requests.get( f"https://api.statuspage.io/v1/pages/{page_id}/incidents/unresolved", headers={"Authorization": f"OAuth {api_key}"})incidents = response.json()if incidents: print(f"🚨 ACTIVE INCIDENT(S):\n") for inc in incidents[:3]: print(f"📌 {inc['name']}") print(f" Status: {inc['status'].upper()}") print(f" Impact: {inc['impact']}") if inc.get('incident_updates'): latest = inc['incident_updates'][0] print(f" Latest update: {latest['body'][:200]}...") print(f" 🔗 Track: {inc['shortlink']}\n") print(f"💡 Recommended response: We're aware of an issue and actively working on it.") print(f" Share the status link above for real-time updates.")else: print(f"✓ No active incidents on status page") print(f"💡 This may be an isolated issue - gather more details:") print(f" • Browser/device?") print(f" • When did it start?") print(f" • Can you share a screenshot?")
Entitlements & Plan Lookup
Pain: “Why can’t I use feature X?” causes long back-and-forth. Agents ask “What plan are you on?” when they could just look it up.User asks:“Why can’t I use SSO?” or “What’s my seat limit?”Integrations: CRM, Product DB, Feature flagsResult: Plan, entitlements, usage limits, what to upgrade, exact remediationImpact: Fewer escalations to Sales/Engineering, faster resolutionTrigger: “When the user asks about their plan, features, entitlements, limits, or why they can’t access something”Parameters:
Name
Type
What to Extract
Required
customer_email
String
Customer’s email or account ID
Yes
feature
String
Feature they’re asking about
No
Secrets:PRODUCT_DB_API_KEY
import osimport requestsemail = os.environ.get('customer_email')feature = os.environ.get('feature', '').lower()api_key = os.environ.get('PRODUCT_DB_API_KEY')# Mock product database lookup - replace with your actual APIresponse = requests.get( f"https://api.yourproduct.com/v1/accounts/lookup", headers={"Authorization": f"Bearer {api_key}"}, params={"email": email})if response.status_code == 200: data = response.json() plan = data.get('plan', 'Free') seats_used = data.get('seats_used', 1) seats_limit = data.get('seats_limit', 5) features = data.get('features', []) print(f"📋 Account: {email}") print(f"📦 Plan: {plan}") print(f"👥 Seats: {seats_used}/{seats_limit} used") print(f"✨ Features enabled: {', '.join(features) or 'Basic only'}") if feature: if feature in [f.lower() for f in features]: print(f"\n✓ '{feature}' IS enabled on this account") else: print(f"\n✗ '{feature}' is NOT included in {plan} plan") print(f"💡 To enable: Upgrade to Pro or Enterprise plan")else: print(f"Account not found for {email}")
Create Jira Bug with Full Context
Pain: Agents write low-quality Jira tickets. Engineers bounce them back asking for steps, logs, and environment details.Agent asks:“Create a Jira bug for this crash with all the details”Integrations: Jira/Linear, Log aggregator, Error trackingResult: Structured bug (steps, expected/actual, env, severity, customer impact)Impact: Fewer loops with engineering, faster MTTR, happier developersTrigger: “When the user asks to create a bug report, Jira issue, or ticket for engineering”Parameters:
Pain: Missed SLAs create churn and fire drills. Agents don’t notice until it’s too late.Leader asks:“Which tickets will breach SLA in the next 2 hours?”Integrations: Ticketing system, SLA policies, Queue metricsResult: Prioritized list + recommended reassignment + breach countdownImpact: 60% fewer SLA breaches, better WBR metrics, reduced churnTrigger: “When the user asks about SLA status, at-risk tickets, or potential SLA breaches”Parameters:
import osimport requestsfrom requests.auth import HTTPBasicAuthfrom datetime import datetime, timedeltahours = int(os.environ.get('hours_ahead', 2))api_token = os.environ.get('ZENDESK_API_TOKEN')subdomain = os.environ.get('ZENDESK_SUBDOMAIN')email = os.environ.get('ZENDESK_EMAIL')# Get tickets with SLA inforesponse = requests.get( f"https://{subdomain}.zendesk.com/api/v2/search.json", auth=HTTPBasicAuth(f"{email}/token", api_token), params={ "query": "type:ticket status<solved", "sort_by": "created_at", "sort_order": "asc", "per_page": 50 })tickets = response.json().get('results', [])# Filter for at-risk (simplified - real impl would use SLA policies API)print(f"⚠️ SLA AT-RISK TICKETS (next {hours} hours):\n")at_risk = []for t in tickets[:20]: created = datetime.fromisoformat(t['created_at'].replace('Z', '+00:00')) age_hours = (datetime.now(created.tzinfo) - created).total_seconds() / 3600 # Simple SLA logic - replace with your actual SLA policies if t.get('priority') == 'urgent' and age_hours > 1: at_risk.append((t, 'CRITICAL', age_hours)) elif t.get('priority') == 'high' and age_hours > 4: at_risk.append((t, 'HIGH', age_hours))if at_risk: for ticket, risk, age in sorted(at_risk, key=lambda x: x[1])[:10]: print(f"🔴 #{ticket['id']}: {ticket['subject'][:50]}...") print(f" Risk: {risk} | Age: {age:.1f}h | Priority: {ticket.get('priority', 'normal')}") print(f" Assignee: {ticket.get('assignee_id', 'Unassigned')}") print(f" 🔗 https://{subdomain}.zendesk.com/agent/tickets/{ticket['id']}\n") print(f"💡 Recommended: Reassign top 3 to available senior agents")else: print(f"✓ No tickets at immediate SLA risk")
Proactive Incident Communications
Pain: During incidents, teams manually compile who is impacted. Customers find out on Twitter before you tell them.Leader asks:“Which customers are impacted by the EU latency issue?”Integrations: Usage telemetry, Region mapping, CRM segmentsResult: Impacted customer list + suggested email template + Zendesk macro updateImpact: Reduces churn by communicating fast and accurately, builds trustTrigger: “When the user asks which customers are affected by an incident, outage, or issue”Parameters:
Name
Type
What to Extract
Required
incident_type
String
Type of incident (latency, outage, feature)
Yes
region
String
Affected region (EU, US, APAC, etc.)
No
Secrets:ANALYTICS_API_KEY
import osimport requestsincident = os.environ.get('incident_type')region = os.environ.get('region', 'all')api_key = os.environ.get('ANALYTICS_API_KEY')# Mock analytics lookup - replace with your telemetry APIprint(f"📊 IMPACT ANALYSIS: {incident.upper()}\n")print(f"Region filter: {region}\n")# Simulated response - replace with actual API callprint(f"👥 Impacted Customers: 47 accounts")print(f" • Enterprise: 12 accounts (prioritize)")print(f" • Pro: 28 accounts")print(f" • Free: 7 accounts\n")print(f"📧 SUGGESTED COMMUNICATION:\n")print(f"Subject: Service Degradation Notice - {incident}")print(f"""Dear Customer,We're currently experiencing {incident} affecting some users in {region}. Our team is actively working on resolution.Current status: InvestigatingETA: Within 2 hoursTrack updates: [statuspage link]We apologize for the inconvenience.""")print(f"💡 Actions:")print(f" 1. Update status page")print(f" 2. Create Zendesk macro for incoming tickets")print(f" 3. Notify CSMs for Enterprise accounts")
Pain: “Where is my order?” is the #1 support question. Customers ask constantly, agents guess ETAs, wrong info causes refunds.Customer asks:“Where is my order #12345?” or “When will my package arrive?”Integrations: OMS, Carrier tracking (FedEx/UPS/USPS), Warehouse statusResult: Real tracking summary, last scan location, expected delivery, exception handlingImpact: Deflects 60%+ of WISMO tickets, reduces refunds from “lost” packagesTrigger: “When the user asks about order status, shipping, tracking, or delivery”Parameters:
Name
Type
What to Extract
Required
order_id
String
Order number or tracking number
Yes
Secrets:SHIPPO_API_KEY
import osimport requestsorder_id = os.environ.get('order_id')api_key = os.environ.get('SHIPPO_API_KEY')# Using Shippo for multi-carrier tracking - replace with your providerresponse = requests.get( f"https://api.goshippo.com/tracks/{order_id}", headers={"Authorization": f"ShippoToken {api_key}"})if response.status_code == 200: data = response.json() status = data.get('tracking_status', {}) print(f"📦 Order: {order_id}") print(f"🚚 Carrier: {data.get('carrier', 'Unknown')}") print(f"📍 Status: {status.get('status', 'Unknown').upper()}") print(f"📍 Location: {status.get('location', {}).get('city', 'In transit')}") eta = data.get('eta') if eta: print(f"📅 Expected delivery: {eta}") # Show tracking history history = data.get('tracking_history', [])[:3] if history: print(f"\n📋 Recent updates:") for event in history: print(f" • {event.get('status_details', 'Update')} - {event.get('status_date', '')[:10]}")else: print(f"📦 Order {order_id}: Processing") print(f"💡 Tracking will be available once shipped (usually within 24-48 hours)")
Account & Billing Self-Service
Pain: Customers can’t find basic account info. “What plan am I on?” “When does my trial end?” flood support.Customer asks:“What plan am I on?” or “When does my subscription renew?”Integrations: Billing system, Subscription managementResult: Plan details, renewal date, usage stats, upgrade optionsImpact: Deflects 40% of account-related tickets, enables self-service upgradesTrigger: “When the user asks about their account, plan, subscription, billing, or renewal”Parameters:
Name
Type
What to Extract
Required
customer_email
String
Customer’s email (from session/memory)
Yes
Secrets:STRIPE_API_KEY
import osimport requestsfrom datetime import datetimeemail = os.environ.get('customer_email')api_key = os.environ.get('STRIPE_API_KEY')# Look up customer in Striperesponse = requests.get( "https://api.stripe.com/v1/customers/search", auth=(api_key, ''), params={"query": f'email:"{email}"'})customers = response.json().get('data', [])if customers: customer = customers[0] # Get subscriptions sub_response = requests.get( "https://api.stripe.com/v1/subscriptions", auth=(api_key, ''), params={"customer": customer['id'], "limit": 1} ) subs = sub_response.json().get('data', []) print(f"👤 Account: {email}\n") if subs: sub = subs[0] plan = sub.get('items', {}).get('data', [{}])[0].get('price', {}).get('nickname', 'Standard') status = sub['status'] period_end = datetime.fromtimestamp(sub['current_period_end']) print(f"📦 Plan: {plan}") print(f"📊 Status: {status.upper()}") print(f"📅 Renews: {period_end.strftime('%B %d, %Y')}") if sub.get('cancel_at_period_end'): print(f"⚠️ Cancels at period end") else: print(f"📦 Plan: Free") print(f"💡 Upgrade anytime at yourapp.com/pricing")else: print(f"No account found for {email}") print(f"💡 Sign up at yourapp.com/signup")
Real-Time Product Availability
Pain: “Is this in stock?” questions are endless. Static KB answers are outdated by the time they’re published.Customer asks:“Is the Blue Widget in stock in size XL?”Integrations: Inventory management, Warehouse systemsResult: Real-time stock level, shipping estimate, alternatives if out of stockImpact: Reduces pre-sales questions 50%, increases conversions with accurate infoTrigger: “When the user asks about stock, inventory, availability, or if a product is in stock”Parameters:
Name
Type
What to Extract
Required
product_name
String
Product name or SKU
Yes
variant
String
Size, color, or other variant
No
Secrets:INVENTORY_API_KEY
import osimport requestsproduct = os.environ.get('product_name')variant = os.environ.get('variant', 'default')api_key = os.environ.get('INVENTORY_API_KEY')# Replace with your inventory APIresponse = requests.get( "https://api.yourinventory.com/v1/stock", headers={"Authorization": f"Bearer {api_key}"}, params={"product": product, "variant": variant})if response.status_code == 200: data = response.json() stock = data.get('quantity', 0) print(f"📦 {product}" + (f" ({variant})" if variant != 'default' else "")) if stock > 10: print(f"✅ In Stock: {stock} available") print(f"🚚 Ships within 1-2 business days") elif stock > 0: print(f"⚠️ Low Stock: Only {stock} left!") print(f"🚚 Order now - ships within 1-2 business days") else: restock = data.get('restock_date', 'soon') print(f"❌ Currently Out of Stock") print(f"📅 Expected back: {restock}") # Suggest alternatives alts = data.get('alternatives', []) if alts: print(f"\n💡 Similar items in stock:") for alt in alts[:3]: print(f" • {alt}")else: print(f"Product not found. Try searching on our website.")
Each Guru has a limit on the number of actions based on your plan (shown as “5 of 10 actions used”).
Before enabling the action, use Test Action to run it with sample parameters. This verifies that parameters, secrets, and the action logic work as expected. Check that the output looks correct and that secrets are masked in the result.
Parameters and secrets are injected as environment variables:
import os# Access a parameter extracted from user's questionusername = os.environ.get('USERNAME')# Access a secret configured in Guru Settingsapi_key = os.environ.get('MY_API_KEY')print(f"Fetching data for {username}...")
The os module is restricted. Only os.environ, os.environ.get(), and os.getenv() are allowed. Other os functions (os.system(), os.popen(), file operations) are blocked.
import osimport requests# 1. Get parameters (extracted from user's question)order_id = os.environ.get('order_id')# 2. Get secrets (configured in Guru Settings)api_key = os.environ.get('STRIPE_API_KEY')# 3. Call your APIresponse = requests.get( "https://api.stripe.com/v1/refunds", auth=(api_key, ''), params={"limit": 10})# 4. Process and print results (this is what the AI uses)data = response.json()print(f"Found {len(data.get('data', []))} refunds")
Use {parameter_name} or {SECRET_NAME} syntax anywhere in the URL, headers, or body. You can also reference the built-in variables {GURUBASE_USER_EMAIL}, {GURUBASE_USER_NAME}, {GURUBASE_USER_GROUPS}, and {GURUBASE_GURU_SLUG}. See Built-in Variables for details and limitations.Example URL with built-in variables:
These names are reserved. You cannot create a parameter or secret named GURUBASE_USER_EMAIL, GURUBASE_USER_NAME, GURUBASE_USER_GROUPS, or GURUBASE_GURU_SLUG. The action will be rejected at validation time.
User variables (GURUBASE_USER_EMAIL, GURUBASE_USER_NAME, GURUBASE_USER_GROUPS) are populated only when the action is called from the Gurubase web UI (chat or Test button). When the action runs via widget, bot integrations (Slack, Discord, Jira, GitHub, Zendesk), MCP, or any API-key-authenticated path, user email and name resolve to empty strings, and groups resolves to Everyone. GURUBASE_GURU_SLUG is always populated. Check for empty values if your action can be triggered from multiple paths.
Secrets are encrypted credentials (API keys, tokens, passwords) stored in Guru Settings → Secrets.
Feature
Description
Encryption
AES-256 encryption at rest
Masking
Automatically masked in all output
Python Access
os.environ.get('SECRET_NAME')
API Call Access
{SECRET_NAME} syntax
Secret names cannot conflict with parameter names. Secret and parameter names also cannot be GURUBASE_USER_EMAIL, GURUBASE_USER_NAME, GURUBASE_USER_GROUPS, or GURUBASE_GURU_SLUG — those are reserved for built-in variables. Deleting a secret automatically disables all actions using it.
Most real workflows don’t fit into a single action. They chain reads with writes, or fan out writes across multiple systems in one turn. Gurubase runs every relevant action per turn, each selected and parameterized independently based on the user’s question.A single turn can include:
Reads to pull live data from a database, CRM, or HR system.
Writes that actually change state in the systems you own (create a ticket, submit a request, post a message).
Parallel fan-out to multiple systems (create a Jira issue and DM an on-call engineer at the same time).
This is the pattern demonstrated in the HR Guru demo video: an employee assistant that reads a PTO balance in one turn, then writes the request to SAP and pings the manager on Slack in the next. One conversation, two action types, three systems.
{ "name": "pto_balance_lookup", "condition_prompt": "When the user asks how many PTO, vacation, or time-off days they have remaining.", "action_type": "PYTHON_CODE", "parameters": [], "config": { "code": "import os\nimport requests\n\nuser = os.environ['GURUBASE_USER_EMAIL']\nr = requests.get(\n f\"{os.environ['HR_API']}/pto\",\n params={'user': user},\n headers={'Authorization': f\"Bearer {os.environ['HR_TOKEN']}\"},\n)\ndata = r.json()\nprint(f\"{data['remaining']} days remaining · last used {data['last_used']}\")\n", "timeout": 30 }}
What it does: Reads the signed-in user’s email from the built-in GURUBASE_USER_EMAIL variable, calls the HR API, prints the balance. The printed output is injected into the agent’s answer.Sample response:“Hi Alex, you have 12 PTO days remaining this year. Last used on March 3, 2026 (2 days).”
Action 2: Create the PTO request in SAP (API call)
submit_pto_request
{ "name": "submit_pto_request", "condition_prompt": "When the user confirms they want to book, submit, or request specific PTO days. Extract the date the user wants off.", "action_type": "API_CALL", "parameters": [ { "name": "date", "type": "string", "description": "The date the user wants to take off, as YYYY-MM-DD.", "required": true } ], "config": { "endpoint": "https://sap.example.com/api/v2/timeoff", "method": "POST", "headers": [ { "key": "Authorization", "value": "Bearer {SAP_API_TOKEN}" }, { "key": "Content-Type", "value": "application/json" } ], "body": { "employee_email": "{GURUBASE_USER_EMAIL}", "start_date": "{date}", "end_date": "{date}", "type": "PTO" } }}
What it does: POSTs to SAP’s time-off endpoint. The signed-in user’s email is pulled from {GURUBASE_USER_EMAIL}, the date comes from the extracted date parameter, and the SAP token is resolved from the encrypted SAP_API_TOKEN secret at execution time.
{ "name": "notify_manager", "condition_prompt": "When a PTO request has been submitted and the user's manager should be notified.", "action_type": "API_CALL", "parameters": [ { "name": "date", "type": "string", "description": "The date being requested off.", "required": true } ], "config": { "endpoint": "https://slack.com/api/chat.postMessage", "method": "POST", "headers": [ { "key": "Authorization", "value": "Bearer {SLACK_BOT_TOKEN}" }, { "key": "Content-Type", "value": "application/json" } ], "body": { "channel": "#hr-approvals", "text": "{GURUBASE_USER_NAME} just requested {date} off via HR Guru." } }}
What it does: Posts a Slack message to the HR approvals channel. The condition prompt is scoped so this fires alongside submit_pto_request (in the same turn), not as an independent action the LLM might pick on its own.
User: How many PTO days do I have left?Agent:(runs pto_balance_lookup) Hi Alex, you have 12 PTO days remaining this year. Last used on March 3, 2026 (2 days).
Turn 2 (parallel writes)
User: Perfect. Book Friday off then.Agent:(runs submit_pto_request and notify_manager in parallel) Done. PTO request #4892 submitted to SAP for Friday, Apr 17. The HR approvals channel has been notified on Slack.
Condition prompts are the glue. Write them specifically enough that notify_manager only fires alongside a real submission, not on every chat about PTO.
Matched actions always run in parallel within a single turn (up to 3 concurrent). This keeps response time low and is the right default when actions are independent, like SAP + Slack above.If you need true data dependencies where the output of action A must feed action B, you have two options:
Multi-turn: Let turn 1 finish, then drive turn 2 with the follow-up question. The conversation carries context between turns, which is how the PTO flow above works.
Agent actions: Wrap the whole sequence in a single AGENT-type action whose internal tool-calling loop runs the steps in order. The agent sees each sub-tool’s output before deciding on the next one.
For most workflows, parallel + multi-turn is plenty. Reach for AGENT only when one user request truly needs a deterministic multi-step pipeline.
Before enabling a multi-action workflow for users:
Test each action standalone from the Action Builder with hand-picked parameters. Every action should succeed in isolation.
Run a full conversation in the Guru preview. Verify the right combination of actions fires at each turn and that parameters extract correctly from the user’s wording.
Review the audit log. Every run records inputs, outputs, duration, and the question that triggered it. This is the fastest way to debug extraction failures or parameter mismatches.
Stress-test condition overlap. If two actions might fire for the same question, either tighten their condition prompts or accept that both will run (and make sure neither has side effects that break the other).